Replace auto paint persistence with a named painting library
Persistence rework: - Drop auto-load on setup and auto-save on teardown; the painter now always seeds fresh from the material. - Named slots under MeshPaint/Saved/<name>.paint via the existing CHMP machinery: SavePaintAs / LoadPaintByName / DeletePaint / ListSavedPaintings / PaintExists. Slot files carry the display name in the header. - Remove the obsolete persistence partials (PlayerCharacterController, PlayerPoseController, ThirdPersonOrbitInput) and their references. Paintings HUD panel: - Authored Panel_Paintings + Save/Load/Delete dialogs into PaintHudPanel.prefab (TMP throughout); PaintHud.Library.cs is now a pure binder that clones the entry template per slot. - Save dialog blocks empty/existing names; Load and Delete confirm first. - Fix dialog z-order: open dialogs SetAsLastSibling so a dragged panel can no longer cover them. - Lib_ListContent horizontal offsets corrected to 0 (was clipping entry labels out of the masked viewport). Multiplayer: - Loading a painting re-syncs to all peers via the snapshot broadcast path (BroadcastLoadedPaint), covering later late-joiners for free. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
1 parent
56caa0edd8
commit
ee482905f9
16 files changed
+4586
-521
No files matched your search
@@ -74,9 +74,6 @@ public class PlayerPaintSync : NetworkBehaviour
|
||||
{
|
||||
// Remote replica: never paint from this machine's mouse; paint only from replicated data.
|
||||
_painter.PaintingEnabled = false;
|
||||
// Don't persist a replicated avatar: all clones share one save key, so remotes would
|
||||
// clobber the local player's saved paint on teardown (same fix as the pose system).
|
||||
_painter.persistPaint = false;
|
||||
|
||||
// Track the owner's emission intensity; seed the current (late-join) value right away.
|
||||
_emissionIntensity.OnValueChanged += OnEmissionChanged;
|
||||
@@ -200,12 +197,12 @@ public class PlayerPaintSync : NetworkBehaviour
|
||||
// Everything the captured maps already contain: the owner's send counter for our own avatar,
|
||||
// otherwise the last batch this server replica has applied.
|
||||
int capturedSeq = IsOwner ? _seq : _seqHorizon;
|
||||
StartCoroutine(SendSnapshot(p.Receive.SenderClientId, data, capturedSeq));
|
||||
StartCoroutine(SendSnapshot(new[] { p.Receive.SenderClientId }, data, capturedSeq));
|
||||
}
|
||||
|
||||
private IEnumerator SendSnapshot(ulong targetClientId, byte[] data, int capturedSeq)
|
||||
private IEnumerator SendSnapshot(ulong[] targets, byte[] data, int capturedSeq)
|
||||
{
|
||||
var send = new ClientRpcParams { Send = new ClientRpcSendParams { TargetClientIds = new[] { targetClientId } } };
|
||||
var send = new ClientRpcParams { Send = new ClientRpcSendParams { TargetClientIds = targets } };
|
||||
|
||||
BeginSnapshotClientRpc(data.Length, capturedSeq, send);
|
||||
|
||||
@@ -252,4 +249,87 @@ public class PlayerPaintSync : NetworkBehaviour
|
||||
if (!_snapReadyToApply)
|
||||
_awaitingSnapshot = false;
|
||||
}
|
||||
|
||||
// ---- Load re-sync (owner -> server -> every other peer) ----
|
||||
//
|
||||
// Loading a saved painting replaces the owner's whole map set at once (no stamps), so the live
|
||||
// stream cannot carry it. The owner captures a snapshot, uploads it to the server chunked (same
|
||||
// throttling as the download), and the server re-broadcasts it to every other peer through the
|
||||
// existing snapshot client RPCs - including the host's own replica via its client-id target. A
|
||||
// cutoff sequence drops any in-flight/buffered stamps already superseded by the loaded baseline.
|
||||
|
||||
private byte[] _uploadBuffer; // server-side reassembly of the owner's uploaded snapshot
|
||||
private int _uploadCutoff;
|
||||
|
||||
/// <summary>Owner-only: broadcast the just-loaded painting to all other peers. No-op offline / single-player.</summary>
|
||||
public void BroadcastLoadedPaint()
|
||||
{
|
||||
if (!IsSpawned || !IsOwner || !_painter) return;
|
||||
if (!_painter.TryCaptureSnapshot(out byte[] data)) return;
|
||||
|
||||
int cutoff = ++_seq; // every stamp up to here is now superseded by the loaded baseline
|
||||
_seqHorizon = cutoff;
|
||||
StartCoroutine(UploadSnapshot(data, cutoff));
|
||||
}
|
||||
|
||||
private IEnumerator UploadSnapshot(byte[] data, int cutoff)
|
||||
{
|
||||
BeginUploadServerRpc(data.Length, cutoff);
|
||||
|
||||
int sentThisFrame = 0;
|
||||
for (int off = 0; off < data.Length; off += snapshotChunkBytes)
|
||||
{
|
||||
int len = Mathf.Min(snapshotChunkBytes, data.Length - off);
|
||||
var chunk = new byte[len];
|
||||
System.Array.Copy(data, off, chunk, 0, len);
|
||||
UploadChunkServerRpc(off, chunk);
|
||||
|
||||
if (++sentThisFrame >= snapshotChunksPerFrame)
|
||||
{
|
||||
sentThisFrame = 0;
|
||||
yield return null; // spread the burst so the reliable window does not overflow
|
||||
}
|
||||
}
|
||||
|
||||
EndUploadServerRpc();
|
||||
}
|
||||
|
||||
[ServerRpc]
|
||||
private void BeginUploadServerRpc(int totalBytes, int cutoff)
|
||||
{
|
||||
_uploadBuffer = totalBytes > 0 ? new byte[totalBytes] : null;
|
||||
_uploadCutoff = cutoff;
|
||||
}
|
||||
|
||||
[ServerRpc]
|
||||
private void UploadChunkServerRpc(int offset, byte[] chunk)
|
||||
{
|
||||
if (_uploadBuffer == null) return;
|
||||
if (offset >= 0 && offset + chunk.Length <= _uploadBuffer.Length)
|
||||
System.Array.Copy(chunk, 0, _uploadBuffer, offset, chunk.Length);
|
||||
}
|
||||
|
||||
[ServerRpc]
|
||||
private void EndUploadServerRpc()
|
||||
{
|
||||
if (_uploadBuffer == null) return;
|
||||
|
||||
// Fan out to every connected peer except the owner (who already loaded it locally). The host,
|
||||
// when it is not the owner, is included by its client id and applies it like any other remote.
|
||||
ulong[] targets = OtherClientIds(OwnerClientId);
|
||||
if (targets.Length > 0)
|
||||
StartCoroutine(SendSnapshot(targets, _uploadBuffer, _uploadCutoff));
|
||||
|
||||
_uploadBuffer = null;
|
||||
}
|
||||
|
||||
/// <summary>All connected client ids except the given one (server-side).</summary>
|
||||
private ulong[] OtherClientIds(ulong except)
|
||||
{
|
||||
var ids = NetworkManager.ConnectedClientsIds;
|
||||
var list = new List<ulong>(ids.Count);
|
||||
for (int i = 0; i < ids.Count; i++)
|
||||
if (ids[i] != except) list.Add(ids[i]);
|
||||
return list.ToArray();
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user