gg.easy.airship 0.1.2194 → 0.1.2195

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (33) hide show
  1. package/Editor/Settings/AirshipSettingsProvider.cs +11 -2
  2. package/Runtime/Code/AirshipConst.cs +3 -2
  3. package/Runtime/Code/Bootstrap/ServerBootstrap.cs +4 -0
  4. package/Runtime/Code/Bundles/SystemRoot.cs +11 -1
  5. package/Runtime/Code/CoreUI/Components/AirshipVersionOverlay.cs +5 -0
  6. package/Runtime/Code/Luau/LuauCore.cs +25 -1
  7. package/Runtime/Code/Luau/LuauCoreCallbacks.cs +6 -0
  8. package/Runtime/Code/Network/Net.cs +9 -3
  9. package/Runtime/Code/Network/Simulation/AirshipSimulationManager.cs +13 -11
  10. package/Runtime/Code/Network/StateSystem/AirshipNetworkedStateManager.cs +118 -109
  11. package/Runtime/Code/Player/Character/MovementSystems/Character/Structures/CharacterSnapshotData.cs +1 -1
  12. package/Runtime/Code/Quality/QualityManager.cs +5 -5
  13. package/Runtime/Code/UI/Canvas/CanvasUIEventInterceptor.cs +14 -14
  14. package/Runtime/Code/VoxelWorld/VoxelBlockDefinition.cs +1 -0
  15. package/Runtime/Code/VoxelWorld/VoxelBlockDefinitionList.cs +1 -0
  16. package/Runtime/Code/VoxelWorld/VoxelBlocks.cs +2 -0
  17. package/Runtime/Code/VoxelWorld/VoxelMeshProcessor.cs +11 -6
  18. package/Runtime/Code/VoxelWorld/VoxelWorld.cs +3 -0
  19. package/Runtime/Plugins/Android/libLuauPlugin.so +0 -0
  20. package/Runtime/Plugins/Linux/libLuauPlugin.so +0 -0
  21. package/Runtime/Plugins/Mac/LuauPlugin.bundle/Contents/Info.plist +7 -7
  22. package/Runtime/Plugins/Mac/LuauPlugin.bundle/Contents/MacOS/LuauPlugin +0 -0
  23. package/Runtime/Plugins/Windows/x64/LuauPlugin.dll +0 -0
  24. package/Runtime/Plugins/iOS/LuauPluginIos.a +0 -0
  25. package/Runtime/Scenes/CoreScene.unity +1 -1
  26. package/ThirdParty/Mirror/Core/NetworkClient.cs +9 -3
  27. package/ThirdParty/Mirror/Core/NetworkClient_TimeInterpolation.cs +1 -1
  28. package/ThirdParty/Mirror/Core/NetworkConnectionToClient.cs +6 -0
  29. package/ThirdParty/Mirror/Core/NetworkServer.cs +6 -4
  30. package/ThirdParty/Mirror/Core/Tools/SimpleMovingAverage.cs +42 -0
  31. package/ThirdParty/Mirror/Core/Tools/SimpleMovingAverage.cs.meta +3 -0
  32. package/URP/AirshipURPAsset.asset +1 -1
  33. package/package.json +1 -1
@@ -1,6 +1,7 @@
1
1
  using UnityEngine;
2
2
  using UnityEditor;
3
3
  using System.Collections.Generic;
4
+ using System.Diagnostics;
4
5
  using System.IO;
5
6
  using Airship.Editor;
6
7
  using Code.Bootstrap;
@@ -186,7 +187,9 @@ public class AirshipSettingsProvider : SettingsProvider
186
187
 
187
188
  if (newTimeout != EditorIntegrationsConfig.instance.luauScriptTimeout) {
188
189
  EditorIntegrationsConfig.instance.luauScriptTimeout = newTimeout;
189
- LuauPlugin.SetScriptTimeoutDuration(newTimeout);
190
+ if (!Debugger.IsAttached) {
191
+ LuauPlugin.SetScriptTimeoutDuration(newTimeout);
192
+ }
190
193
  }
191
194
 
192
195
  if (GUI.changed) {
@@ -252,6 +255,12 @@ public class AirshipSettingsProvider : SettingsProvider
252
255
 
253
256
  [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.AfterSceneLoad)]
254
257
  private static void OnReload() {
255
- LuauPlugin.SetScriptTimeoutDuration(EditorIntegrationsConfig.instance.luauScriptTimeout);
258
+ if (Debugger.IsAttached) {
259
+ // If the debugger is attached, just feed in a large delay to effectively disable the timeout.
260
+ // In the future, we could adjust the plugin to take something like -1 to disable it.
261
+ LuauPlugin.SetScriptTimeoutDuration(86400);
262
+ } else {
263
+ LuauPlugin.SetScriptTimeoutDuration(EditorIntegrationsConfig.instance.luauScriptTimeout);
264
+ }
256
265
  }
257
266
  }
@@ -6,17 +6,18 @@ using UnityEngine.Scripting;
6
6
  namespace Code {
7
7
  [LuauAPI][Preserve]
8
8
  public static class AirshipConst {
9
- public const int playerVersion = 21;
9
+ public const int playerVersion = 23;
10
10
  public static readonly IReadOnlyList<string> playerFlags = new string[] {
11
11
  "SkipLoading",
12
12
  "LagCompCheckIdIsInt",
13
13
  "PlatformGearDownloadClassId",
14
+ "HasTransformMoveDirection", // True for versions that have access to CharacterMovement.TransformMoveDirection
14
15
  };
15
16
 
16
17
 
17
18
  /// <summary>
18
19
  /// The server will kick clients that have a playerVersion lower than this value.
19
20
  /// </summary>
20
- public const int minAcceptedPlayerVersionOnServer = 21;
21
+ public const int minAcceptedPlayerVersionOnServer = 23;
21
22
  }
22
23
  }
@@ -66,6 +66,9 @@ public class ServerBootstrap : MonoBehaviour
66
66
  private GameServer gameServer;
67
67
 
68
68
  [NonSerialized] public bool isServerReady = false;
69
+ /// <summary>
70
+ /// Can be fired multiple times.
71
+ /// </summary>
69
72
  public event Action OnStartLoadingGame;
70
73
  public event Action OnServerReady;
71
74
  public event Action OnStartupConfigReady;
@@ -373,6 +376,7 @@ public class ServerBootstrap : MonoBehaviour
373
376
  var request = UnityWebRequestProxyHelper.ApplyProxySettings(new UnityWebRequest(url));
374
377
  var gameConfigPath = Path.Combine(Application.persistentDataPath, "Games", startupConfig.GameBundleId, "gameConfig.json");
375
378
  request.downloadHandler = new DownloadHandlerFile(gameConfigPath);
379
+ request.timeout = 7;
376
380
  yield return request.SendWebRequest();
377
381
  if (request.result != UnityWebRequest.Result.Success) {
378
382
  Debug.LogError($"Failed to download gameConfig.json. url={url}, message={request.error}");
@@ -175,15 +175,25 @@ public class SystemRoot : Singleton<SystemRoot> {
175
175
  var sw = Stopwatch.StartNew();
176
176
 
177
177
  // Find packages we should UNLOAD
178
- List<string> unloadList = new();
178
+ HashSet<string> unloadList = new();
179
179
  foreach (var loadedPair in this.loadedAssetBundles) {
180
180
  if (forceUnloadAll) {
181
181
  unloadList.Add(loadedPair.Key);
182
182
  continue;
183
183
  }
184
+
184
185
  var packageToLoad = packages.Find(p => p.id.ToLower() == loadedPair.Value.airshipPackage.id.ToLower());
186
+ // Todo: we can not unload asset bundles if there is only a code change.
185
187
  if (packageToLoad == null || packageToLoad.assetVersion != loadedPair.Value.airshipPackage.assetVersion || packageToLoad.codeVersion != loadedPair.Value.airshipPackage.codeVersion) {
186
188
  unloadList.Add(loadedPair.Key);
189
+
190
+ // If a package changes, then we need to unload everything because other packages & the game may depend on it.
191
+ if (packageToLoad != null && packageToLoad.packageType == AirshipPackageType.Package) {
192
+ foreach (var loaded in loadedAssetBundles) {
193
+ unloadList.Add(loaded.Key);
194
+ }
195
+ break;
196
+ }
187
197
  }
188
198
  }
189
199
 
@@ -14,6 +14,11 @@ namespace Code.CoreUI.Components {
14
14
  }
15
15
  #endif
16
16
 
17
+ #if UNITY_IOS || UNITY_ANDROID
18
+ this.versionText.enabled = false;
19
+ return;
20
+ #endif
21
+
17
22
  var hash = AirshipVersion.GetVersionHash();
18
23
  this.versionText.text = Application.version + "-" + hash;
19
24
  }
@@ -258,11 +258,35 @@ public partial class LuauCore : MonoBehaviour {
258
258
  }
259
259
 
260
260
  #if UNITY_EDITOR
261
+ private PauseState _editorPauseState = PauseState.Unpaused;
262
+ private bool _appPaused = false;
263
+ private bool _pluginPaused = false;
264
+
265
+ private void OnApplicationOrEditorPauseChanged() {
266
+ var shouldPause = _appPaused || _editorPauseState == PauseState.Paused;
267
+ if (shouldPause == _pluginPaused) return;
268
+
269
+ _pluginPaused = shouldPause;
270
+ LuauPlugin.SetIsPaused(_pluginPaused);
271
+ }
272
+
261
273
  private void OnPauseStateChanged(PauseState state) {
262
- LuauPlugin.SetIsPaused(state == PauseState.Paused);
274
+ // Handle pauses in-editor triggered by the toolbar pause button
275
+ _editorPauseState = state;
276
+ OnApplicationOrEditorPauseChanged();
263
277
  }
264
278
  #endif
265
279
 
280
+ private void OnApplicationPause(bool pauseStatus) {
281
+ // Handle application pauses (e.g. when application loses focus on iOS)
282
+ #if UNITY_EDITOR
283
+ _appPaused = pauseStatus;
284
+ OnApplicationOrEditorPauseChanged();
285
+ #else
286
+ LuauPlugin.SetIsPaused(pauseStatus);
287
+ #endif
288
+ }
289
+
266
290
  private void Start() {
267
291
  Application.quitting += Quit;
268
292
  LuauPlugin.unityMainThreadId = Thread.CurrentThread.ManagedThreadId;
@@ -717,6 +717,12 @@ public partial class LuauCore : MonoBehaviour {
717
717
  }
718
718
 
719
719
  private static unsafe void SetFieldValue<T>(object instance, T value, FieldInfo fieldInfo) where T : unmanaged {
720
+ // If declaring type is not blittable just use reflection
721
+ if (!UnsafeUtility.IsBlittable(fieldInfo.DeclaringType)) {
722
+ fieldInfo.SetValue(instance, value);
723
+ return;
724
+ }
725
+
720
726
  if (fieldInfo.IsStatic) {
721
727
  // Not sure how to do non-alloc static field sets, so just use reflection for now
722
728
  // (these are relatively rare anyways)
@@ -1,6 +1,7 @@
1
1
  using System;
2
2
  using System.Collections.Generic;
3
3
  using Code.Luau;
4
+ using Code.Zstd;
4
5
  using Mirror;
5
6
  using UnityEngine;
6
7
  using UnityEngine.Scripting;
@@ -75,11 +76,16 @@ namespace Assets.Luau.Network {
75
76
 
76
77
  private void OnBroadcastFromClient(NetworkConnectionToClient conn, NetBroadcast msg) {
77
78
  // Runs on the server, when the client broadcasts a message
78
- if ((ulong)msg.Blob.DecompressedDataSize >= MaxBytesAtOnce) {
79
- Debug.LogWarning($"Dropping message from client connection {conn.connectionId} due to exceeding max data size.");
79
+ try {
80
+ if ((ulong)msg.Blob.DecompressedDataSize >= MaxBytesAtOnce) {
81
+ Debug.LogWarning(
82
+ $"Dropping message from client connection {conn.connectionId} due to exceeding max data size.");
83
+ return;
84
+ }
85
+ } catch (ZstdException) { // TODO: temporary code to discard Zstd exceptions
80
86
  return;
81
87
  }
82
-
88
+
83
89
  var now = Time.realtimeSinceStartup;
84
90
  if (!_throttle.TryGetValue(conn.connectionId, out var throttle)) {
85
91
  // If we don't have a throttle key, that means the client isn't connected anymore and we are processing an old message.
@@ -205,10 +205,18 @@ namespace Code.Network.Simulation
205
205
 
206
206
  // Only resimulate once. Go back to the farthest back time that was requested.
207
207
  if (resimBackTo != tick) this.PerformResimulation(resimBackTo);
208
+
209
+ // Perform the standard tick behavior
210
+ OnTick?.Invoke(tick, time, false);
211
+ // Debug.Log($"Simulate call. Main tick {tick}");
212
+ Physics.Simulate(Time.fixedDeltaTime);
213
+ OnCaptureSnapshot?.Invoke(tick, time, false);
208
214
 
209
215
  // Process any lag compensation requests now that we have completed the ticking and snapshot creation
210
216
  // Note: This process is placed after snapshot processing so that changes made to physics (like an impulse)
211
- // are processed on the _next_ tick. This is safe because the server never resimulates.
217
+ // are processed on the _next_ tick. This is safe because the server never resimulates. If lag compensation
218
+ // is placed before OnTick, lag compensation may overwrite changes that occured during Update() or LateUpdate()
219
+ // since we roll back to the last snapshot which will have occured before Update() and LateUpdate().
212
220
  var processedLagCompensation = false;
213
221
  foreach (var entry in this.lagCompensationRequests)
214
222
  {
@@ -216,9 +224,9 @@ namespace Code.Network.Simulation
216
224
  {
217
225
  processedLagCompensation = true;
218
226
  // Debug.LogWarning("Server lag compensation rolling back for client " + entry.Key.connectionId);
219
- // commandBufferTime is the additional time we queue commands locally on the server before processing them
220
- var commandBufferTime = (NetworkServer.sendInterval * (entry.Key.bufferTimeMultiplier / 2f));
221
- OnLagCompensationCheck?.Invoke(entry.Key.connectionId, previousTicks[^1], previousTimes[^1], entry.Key.rtt / 2f, entry.Key.bufferTime + commandBufferTime);
227
+ // inputBufferTime is the additional time we queue commands locally on the server before processing them
228
+ // bufferTime is the observed player buffer size on the client
229
+ OnLagCompensationCheck?.Invoke(entry.Key.connectionId, tick, time, entry.Key.rtt / 2f, entry.Key.bufferTime + entry.Key.inputBufferTime);
222
230
 
223
231
  var requests = entry.Value;
224
232
  for (var i = 0; i < requests.Count; i++) {
@@ -236,7 +244,7 @@ namespace Code.Network.Simulation
236
244
  {
237
245
  // Debug.LogWarning("Server completed " + this.lagCompensationRequests.Count + " lag compensation requests. Resetting to current tick (" + previousTimes[^1] + ") and finalizing.");
238
246
  // Reset back to the server view of the world at the current time.
239
- OnSetSnapshot?.Invoke(previousTicks[^1]);
247
+ OnSetSnapshot?.Invoke(tick);
240
248
  Physics.SyncTransforms();
241
249
  // Invoke all of the callbacks for modifying physics that should be applied in the next tick.
242
250
  foreach (var entry in this.lagCompensationRequests)
@@ -255,12 +263,6 @@ namespace Code.Network.Simulation
255
263
  }
256
264
  this.lagCompensationRequests.Clear();
257
265
  }
258
-
259
- // Perform the standard tick behavior
260
- OnTick?.Invoke(tick, time, false);
261
- // Debug.Log($"Simulate call. Main tick {tick}");
262
- Physics.Simulate(Time.fixedDeltaTime);
263
- OnCaptureSnapshot?.Invoke(tick, time, false);
264
266
 
265
267
  // Add our completed tick time into our history
266
268
  this.previousTicks.Add(tick);
@@ -68,16 +68,21 @@ namespace Code.Network.StateSystem
68
68
  private int serverLastProcessedCommandNumber;
69
69
  private int serverPredictedCommandCount = 0;
70
70
  private SortedList<int, Input> serverCommandBuffer = new SortedList<int, Input>();
71
+ // Used to flag if we have exhaused the command buffer and need to wait for it to reach target fill
72
+ private bool serverCommandBufferWait = true;
73
+ private int serverNextBufferCheckTick = 0;
71
74
 
72
75
  private int serverCommandBufferMaxSize = 0;
73
76
 
74
77
  // How many commands we should generally have in the command buffer
75
- private int serverCommandBufferTargetSize = 0;
76
78
  private int serverCommandCatchUpRequired = 0;
79
+ private SimpleMovingAverage serverCommandBufferTargetSize;
80
+ private SimpleMovingAverage serverCommandBufferAvgSize;
77
81
 
78
82
  // Non-auth server command tracking
79
83
  // Note: we also re-use some of the above command buffer fields
80
84
  private SortedList<int, State> serverReceivedStateBuffer = new SortedList<int, State>();
85
+ private int serverStatesReceivedThisFrame = 0;
81
86
  // Last observed state from OnTick when running as a non-auth server.
82
87
  private State serverLastObservedState;
83
88
 
@@ -203,10 +208,8 @@ namespace Code.Network.StateSystem
203
208
  // The client should also stop sending commands after 1 second's worth of unconfirmed commands.
204
209
  // This value is refreshed in auth server tick
205
210
  this.serverCommandBufferMaxSize = (int)(1f/ Time.fixedUnscaledDeltaTime);
206
- // must convert send interval to scaled time because fixedDeltaTime is scaled
207
- // This value is refreshed in auth server tick
208
- this.serverCommandBufferTargetSize =Math.Min(this.serverCommandBufferMaxSize,
209
- (int)Math.Ceiling(NetworkServer.sendInterval * bufferTimeMultiplier / Time.fixedUnscaledDeltaTime));
211
+ this.serverCommandBufferTargetSize = new SimpleMovingAverage(NetworkServer.sendRate);
212
+ this.serverCommandBufferAvgSize = new SimpleMovingAverage(NetworkServer.sendRate);
210
213
 
211
214
  this.inputHistory = new(1);
212
215
  this.stateHistory = new(1);
@@ -346,26 +349,44 @@ namespace Code.Network.StateSystem
346
349
  this.Interpolate();
347
350
  }
348
351
 
349
- // We check this in Update so that we have finished processing all required fixedUpdates before checking
350
- // if we are behind. Next time fixed update runs, we will process the additional amount we need to catch up.
351
- if (isServer && serverAuth) {
352
- if (serverCommandBuffer.Count > serverCommandBufferTargetSize) {
353
- serverCommandCatchUpRequired = serverCommandBuffer.Count - serverCommandBufferTargetSize;
354
- // print($"Command catchup required for {this.name}: {serverCommandCatchUpRequired}");
355
- }
356
- else {
357
- serverCommandCatchUpRequired = 0;
358
- }
352
+ // Server states are sent one at a time instead of as a group, but we track how many we receive per frame so we can adjust our
353
+ // target buffer size if the client is slow. This is similar to what we do for received input groups.
354
+ if (serverStatesReceivedThisFrame != 0) {
355
+ // var jitterTime = Math.Sqrt(this.connectionToClient.rttVariance) / Time.fixedUnscaledDeltaTime;
356
+ this.serverCommandBufferTargetSize.Add(serverStatesReceivedThisFrame);
357
+ serverStatesReceivedThisFrame = 0;
359
358
  }
360
-
361
- // Same as above, but for client authoritative systems
362
- if (isServer && !serverAuth) {
363
- if (serverReceivedStateBuffer.Count > serverCommandBufferTargetSize) {
364
- serverCommandCatchUpRequired = serverReceivedStateBuffer.Count - serverCommandBufferTargetSize;
365
- // print($"State catchup required for {this.name}: {serverCommandCatchUpRequired}");
359
+
360
+ if (AirshipSimulationManager.Instance.tick > this.serverNextBufferCheckTick) {
361
+ this.serverNextBufferCheckTick = AirshipSimulationManager.Instance.tick + NetworkServer.sendRate;
362
+
363
+ // We check this in Update so that we have finished processing all required fixedUpdates before checking
364
+ // if we are behind. Next time fixed update runs, we will process the additional amount we need to catch up.
365
+ if (isServer && serverAuth) {
366
+ if (serverCommandBuffer.Count > this.serverCommandBufferTargetSize.Value && Math.Floor(this.serverCommandBufferAvgSize.Value) > Math.Ceiling(this.serverCommandBufferTargetSize.Value)) {
367
+ serverCommandCatchUpRequired = (int) Math.Round(Math.Floor(this.serverCommandBufferAvgSize.Value) - Math.Ceiling(this.serverCommandBufferTargetSize.Value));
368
+ // print($"Command catchup required for {this.name}: {serverCommandCatchUpRequired}. {serverCommandBuffer.Count} in buffer {(int) Math.Round(this.serverCommandBufferTargetSize.Value)} target");
369
+ } else if (this.serverCommandBufferAvgSize.Value * 2 < this.serverCommandBufferTargetSize.Value) {
370
+ // This is a heuristic for if we are likely to have to predict more commands soon. Low buffer size means
371
+ // we have less of a chance to recover dropped packets. It's probably better for us to wait for additional
372
+ // inputs before we continue processing.
373
+ // print($"Command delay required for {this.name}: {serverCommandBuffer.Count} in buffer, {this.serverCommandBufferAvgSize.Value} ema, {this.serverCommandBufferTargetSize.Value} target");
374
+ this.serverCommandBufferWait = true;
375
+ } else {
376
+ serverCommandCatchUpRequired = 0;
377
+ }
366
378
  }
367
- else {
368
- serverCommandCatchUpRequired = 0;
379
+
380
+ // Same as above, but for client authoritative systems
381
+ if (isServer && !serverAuth) {
382
+ if (serverReceivedStateBuffer.Count > this.serverCommandBufferTargetSize.Value && this.serverCommandBufferAvgSize.Value > this.serverCommandBufferTargetSize.Value) {
383
+ serverCommandCatchUpRequired = (int) Math.Round(this.serverCommandBufferAvgSize.Value - this.serverCommandBufferTargetSize.Value);
384
+ // print($"State catchup required for {this.name}: {serverCommandCatchUpRequired}");
385
+ } else if (this.serverCommandBufferAvgSize.Value * 2 < this.serverCommandBufferTargetSize.Value) {
386
+ this.serverCommandBufferWait = true;
387
+ } else {
388
+ serverCommandCatchUpRequired = 0;
389
+ }
369
390
  }
370
391
  }
371
392
  }
@@ -523,7 +544,7 @@ namespace Code.Network.StateSystem
523
544
 
524
545
  #region Authoritative Server Functions
525
546
 
526
- public void AuthServerTick(int tick, double time, bool replay)
547
+ public void AuthServerTick(int tick, double time, bool replay)
527
548
  {
528
549
  if (replay)
529
550
  {
@@ -544,30 +565,20 @@ namespace Code.Network.StateSystem
544
565
 
545
566
  // We must recalculate target size if the timescale has changed.
546
567
  this.serverCommandBufferMaxSize = (int)( 1 / Time.fixedUnscaledDeltaTime);
547
- this.serverCommandBufferTargetSize =
548
- Math.Min(this.serverCommandBufferMaxSize,
549
- (int)Math.Ceiling(NetworkServer.sendInterval * this.bufferTimeMultiplier / Time.fixedUnscaledDeltaTime));
550
568
  // Optimal max is when we will start processing extra commands.
551
- // print($"{this.name} has {serverCommandBuffer.Count} entries in the buffer. Target is {this.serverCommandBufferTargetSize}");
552
-
553
- // If we don't allow command catchup, drop commands to get to the target buffer size.
554
- if (this.maxServerCommandCatchup == 0)
555
- {
556
- var dropCount = 0;
557
-
558
- while (serverCommandCatchUpRequired > 0 && dropCount < this.serverCommandBufferTargetSize)
559
- {
560
- this.serverCommandBuffer.RemoveAt(0);
561
- dropCount++;
562
- }
563
- // print("Dropped " + dropCount + " command(s) from " + this.gameObject.name + " due to exceeding command buffer size.");
564
- }
569
+ this.serverCommandBufferAvgSize.Add(this.serverCommandBuffer.Count);
570
+ this.connectionToClient.inputBufferTime = this.serverCommandBufferAvgSize.Value * Time.fixedUnscaledDeltaTime;
571
+ // print($"{this.name} has {serverCommandBuffer.Count} entries in the buffer. Target is {this.serverCommandBufferTargetSize.Value} Current Avg Fill: {this.serverCommandBufferAvgSize.Value}");
565
572
 
566
573
  // Delay processing until we have at least one send interval worth of commands to process.
567
- if (this.serverCommandBuffer.Count == 0 || this.serverCommandBuffer.Count < Math.Ceiling(NetworkClient.sendInterval / Time.fixedUnscaledDeltaTime)) {
574
+ if (this.serverCommandBufferWait && this.serverCommandBuffer.Count < this.serverCommandBufferTargetSize.Value || this.serverCommandBuffer.Count == 0) {
568
575
  // Debug.Log($"Waiting for additional commands for {this.name}. There are {this.serverCommandBuffer.Count} commands in the buffer.");
569
576
  this.stateSystem.Tick(null, tick, time, false);
570
577
  return;
578
+ } else {
579
+ // Start processing commands from the beginning of the buffer after waiting
580
+ this.serverLastProcessedCommandNumber = this.serverCommandBuffer.Values[0].commandNumber - 1;
581
+ this.serverCommandBufferWait = false;
571
582
  }
572
583
 
573
584
  var commandsProcessed = 0;
@@ -575,69 +586,62 @@ namespace Code.Network.StateSystem
575
586
  commandsProcessed++;
576
587
 
577
588
  // Attempt to get a new command out of the buffer.
578
- var commandEntry = this.serverCommandBuffer.Count > 0 ? this.serverCommandBuffer.Values[0] : null;
579
-
580
- // If we have a new command to process
581
- if (commandEntry != null) {
589
+ var canPredictCommand = this.serverPredictedCommandCount < Math.Ceiling(
590
+ this.maxServerCommandPrediction *
591
+ (NetworkServer.sendInterval /
592
+ Time.fixedUnscaledDeltaTime));
593
+ var expectedNextCommandNumber = this.serverLastProcessedCommandNumber + 1;
594
+ Input command = this.serverCommandBuffer.Count > 0 ? this.serverCommandBuffer.Values[0] : null;
595
+
596
+ // We have a valid command that is in sequence, or we reached our max predicted fill. Remove the next
597
+ // valid command from the buffer and process it.
598
+ if (command != null && (command.commandNumber == expectedNextCommandNumber || !canPredictCommand)) {
582
599
  // Get the command to process
583
- var command = this.serverCommandBuffer.Values[0];
584
- // Get the expected next command number
585
- var expectedNextCommandNumber = this.serverLastProcessedCommandNumber + 1;
586
-
587
- // Check if that command is in sequence. If we have a gap of commands, we will fill it with the last
588
- // processed command up to the maxServerCommandPrediction size.
589
- // this.maxServerCommandPrediction * (1f / this.clientInputRate / Time.fixedDeltaTime)) is the number of
590
- // commands contained in a single input message
591
- if (this.lastProcessedCommand != null && command.commandNumber != expectedNextCommandNumber &&
592
- this.serverPredictedCommandCount < Math.Ceiling(this.maxServerCommandPrediction *
593
- (NetworkServer.sendInterval /
594
- Time.fixedUnscaledDeltaTime))) {
595
- // Debug.LogWarning("Missing command " + expectedNextCommandNumber +
596
- // " in the command buffer for " + this.name + ". Next command was: " +
597
- // command.commandNumber +
598
- // ". Predicted " +
599
- // (this.serverPredictedCommandCount + 1) + " command(s) so far.");
600
- this.serverLastProcessedCommandNumber = expectedNextCommandNumber;
601
- command = this.lastProcessedCommand;
602
- command.commandNumber = expectedNextCommandNumber;
603
- this.serverPredictedCommandCount++;
604
- }
605
- // We have a valid command that is in sequence, or we reached our max fill. Remove the next
606
- // valid command from the buffer and process it.
607
- else {
608
- // Debug.Log("Ticking next command in sequence: " + command.commandNumber);
609
- this.serverPredictedCommandCount = 0;
610
- this.serverCommandBuffer.RemoveAt(0);
611
- this.lastProcessedCommand = command;
612
- this.serverLastProcessedCommandNumber = command.commandNumber;
613
- }
614
-
615
- // tick command
616
- command.tick = tick; // Correct tick to local timeline for ticking on the server.
617
- this.stateSystem.Tick(command, tick, time, false);
600
+ command = this.serverCommandBuffer.Values[0];
601
+ this.serverCommandBuffer.RemoveAt(0);
602
+
603
+ this.serverPredictedCommandCount = 0;
604
+ this.lastProcessedCommand = command;
605
+ this.serverLastProcessedCommandNumber = command.commandNumber;
606
+ // We have an out of order command, or no command and we can predict the next command in sequence
607
+ } else if ((command != null && command.commandNumber != expectedNextCommandNumber ||
608
+ this.serverCommandBuffer.Count == 0) && this.lastProcessedCommand != null && canPredictCommand) {
609
+ command = this.lastProcessedCommand;
610
+
611
+ this.serverLastProcessedCommandNumber = expectedNextCommandNumber;
612
+
613
+ command.commandNumber = expectedNextCommandNumber;
614
+ this.serverPredictedCommandCount++;
615
+ // print("Reprocessing last command");
618
616
  }
617
+ // we processed a command that never reached the server and we can't fill it or move on to a new command in the buffer.
618
+ // this means we've completely run out of inputs and we should wait for more.
619
619
  else {
620
- // Ensure that we always tick the system even if there's no command to process.
621
- // Debug.LogWarning($"No commands left for {this.name}. Last command processed: " +
622
- // this.lastProcessedCommand);
623
- this.stateSystem.Tick(null, tick, time, false);
624
- // we processed a command that never reached the server, advance so the associated
625
- // command's tick result will be used to match up with state. The command that should have been used
626
- // here will be ignored when it arrives (if it ever does)
627
- this.serverLastProcessedCommandNumber += 1;
620
+ // print("No command available for ticking");
621
+ this.serverPredictedCommandCount = 0;
622
+ this.serverCommandBufferWait = true;
623
+ }
624
+
625
+ // tick command
626
+ // Ensure that we always tick the system even if there's no command to process.
627
+ if (command != null) {
628
+ command.tick = tick; // Correct tick to local timeline for ticking on the server.
629
+ }
630
+
631
+ // We cannot tick more than once per FixedUpdate with our current implementation of character movement. A physics.simulate
632
+ // is required to apply the calculated velocities and move the character. We could work around this by pausing all other
633
+ // characters and then simulating forward once, but it would add a lot of complexity/processing power requirements.
634
+ // Instead we just drop the command and don't process it at all.
635
+ if (commandsProcessed == 1) {
636
+ this.stateSystem.Tick(command, tick, time, false);
628
637
  }
629
638
 
630
639
  if (commandsProcessed > 1) {
631
640
  serverCommandCatchUpRequired--;
641
+ // print($"Processed additional command for catchup. {serverCommandCatchUpRequired} more required.");
632
642
  }
633
643
 
634
- } while (commandsProcessed < 1 + this.maxServerCommandCatchup && serverCommandCatchUpRequired > 0);
635
- // We add 1 to maxServerCommandCatchup because we always want to process at least 1 command per fixed update.
636
-
637
- // if (commandsProcessed > 1)
638
- // {
639
- // print("Processed " + commandsProcessed + " commands for " + this.gameObject.name + $". There are now {this.serverCommandBuffer.Count} commands in the buffer.");
640
- // }
644
+ } while (commandsProcessed < 1 + (this.serverCommandBufferTargetSize.Value / 2) && serverCommandCatchUpRequired > 0);
641
645
  }
642
646
 
643
647
  public void AuthServerCaptureSnapshot(int tick, double time, bool replay)
@@ -751,17 +755,18 @@ namespace Code.Network.StateSystem
751
755
  }
752
756
 
753
757
  this.serverCommandBufferMaxSize = (int)( 1 / Time.fixedUnscaledDeltaTime);
754
- this.serverCommandBufferTargetSize =
755
- Math.Min(this.serverCommandBufferMaxSize,
756
- (int)Math.Ceiling(NetworkServer.sendInterval * bufferTimeMultiplier / Time.fixedUnscaledDeltaTime));
757
- // print($"{this.name} {serverReceivedStateBuffer.Count}/{serverCommandBufferMaxSize} target {serverCommandBufferTargetSize}");
758
+ this.serverCommandBufferAvgSize.Add(this.serverReceivedStateBuffer.Count);
759
+ this.connectionToClient.inputBufferTime = this.serverCommandBufferAvgSize.Value * Time.fixedUnscaledDeltaTime;
760
+ // print($"{this.name} {serverReceivedStateBuffer.Count}/{serverCommandBufferMaxSize} target {serverCommandBufferTargetSize.Value} avg {serverCommandBufferAvgSize.Value}");
758
761
 
759
762
  // Delay processing until we have at least one send interval worth of commands to process.
760
- if (this.serverReceivedStateBuffer.Count == 0 || this.serverReceivedStateBuffer.Count < (int)Math.Ceiling(NetworkClient.sendInterval / Time.fixedUnscaledDeltaTime)) {
763
+ if (this.serverCommandBufferWait && this.serverReceivedStateBuffer.Count < this.serverCommandBufferTargetSize.Value || this.serverReceivedStateBuffer.Count == 0) {
761
764
  // Debug.Log($"Waiting for additional states for {this.name}. There are {this.serverReceivedStateBuffer.Count} states in the buffer.");
762
765
  // no operation since there is no new state for us to use. Client authority means we use whatever the client sends us, even if that means
763
766
  // seeing irregular physics movement.
764
767
  return;
768
+ } else {
769
+ this.serverCommandBufferWait = false;
765
770
  }
766
771
 
767
772
  // Process the buffer of states that we've gotten from the authoritative client
@@ -781,14 +786,14 @@ namespace Code.Network.StateSystem
781
786
  : null;
782
787
 
783
788
  // If we have a new state to process, update our last processed command and then remove it.
789
+ // TODO: We could be a little more clever with gaps in received state. We could interpolate between last and next if one exists
784
790
  if (latestState != null) {
785
791
  // mark this as our latest state and remove it. We will do the real processing on the final
786
792
  // state retrieved during this loop later.
787
793
  this.serverLastProcessedCommandNumber = latestState.lastProcessedCommand;
788
794
  this.serverReceivedStateBuffer.RemoveAt(0);
789
- }
790
- else {
791
- this.serverLastProcessedCommandNumber += 1;
795
+ } else {
796
+ this.serverCommandBufferWait = true;
792
797
  }
793
798
 
794
799
  if (statesProcessed > 1) {
@@ -797,7 +802,7 @@ namespace Code.Network.StateSystem
797
802
 
798
803
  // If we don't have a new state to process, that's ok. It just means that the client hasn't sent us
799
804
  // their updated state yet.
800
- } while (statesProcessed < 1 + this.maxServerCommandCatchup && serverCommandCatchUpRequired > 0);
805
+ } while (statesProcessed < 1 + (this.serverCommandBufferTargetSize.Value / 2) && serverCommandCatchUpRequired > 0);
801
806
 
802
807
  // Commit the last processed snapshot to our state history
803
808
  if (latestState != null)
@@ -1103,6 +1108,7 @@ namespace Code.Network.StateSystem
1103
1108
  // interpolate over unscaledTime accurately. The remote timestamp is what the server was rendering
1104
1109
  // at that time, which may be the same tick twice (especially with modified timescales)
1105
1110
  if (!isOwned) {
1111
+ // print($"Frame {Time.frameCount} received state for {state.time}");
1106
1112
  this.observerHistory.Set(state.time, state);
1107
1113
  }
1108
1114
 
@@ -1220,8 +1226,10 @@ namespace Code.Network.StateSystem
1220
1226
  this.serverCommandBuffer.Add(command.commandNumber, command);
1221
1227
  }
1222
1228
 
1223
- private void ServerReceiveInputCommand(Input[] commands)
1224
- {
1229
+ private void ServerReceiveInputCommand(Input[] commands) {
1230
+ // var jitterTime = Math.Sqrt(this.connectionToClient.rttVariance) / Time.fixedUnscaledDeltaTime;
1231
+ serverCommandBufferTargetSize.Add(commands.Length);
1232
+ // print($"{commands.Length} + {jitterTime}");
1225
1233
  foreach (var command in commands)
1226
1234
  {
1227
1235
  ProcessClientInputOnServer(command);
@@ -1229,8 +1237,9 @@ namespace Code.Network.StateSystem
1229
1237
 
1230
1238
  }
1231
1239
 
1232
- private void ServerReceiveSnapshot(State snapshot)
1233
- {
1240
+ private void ServerReceiveSnapshot(State snapshot) {
1241
+ serverStatesReceivedThisFrame++;
1242
+
1234
1243
  // Debug.Log("Server receive snapshot" + snapshot.lastProcessedCommand + " data: " + snapshot.ToString());
1235
1244
  // This should only occur if the server is not authoritative.
1236
1245
  if (serverAuth) return;
@@ -258,7 +258,7 @@ namespace Code.Player.Character.MovementSystems.Character
258
258
  // we could not do this optimization since there would be no way to know where the next packet starts.
259
259
  if (fullCustomData && other.customData != null) { // If we should send the full new data, send it, but only if we actually have data. null custom data shouldn't be sent.
260
260
  writer.WriteBytes(other.customData.Data, 0, other.customData.Data.Length);
261
- } else {
261
+ } else if (customDataDiff != null) {
262
262
  writer.WriteBytes(customDataDiff, 0, customDataDiff.Length);
263
263
  }
264
264
 
@@ -101,7 +101,7 @@ namespace Code.Quality {
101
101
  private int GetRealTargetFPS() {
102
102
  var targetFrameRate = Application.targetFrameRate;
103
103
  if (targetFrameRate < 0 || targetFrameRate > Screen.currentResolution.refreshRateRatio.value)
104
- targetFrameRate = (int) (1.0 / Screen.currentResolution.refreshRateRatio.value);
104
+ targetFrameRate = (int) Screen.currentResolution.refreshRateRatio.value;
105
105
  return targetFrameRate;
106
106
  }
107
107
 
@@ -115,16 +115,16 @@ namespace Code.Quality {
115
115
  var avgFrameTimings = GetRecentAverageFrameTimings();
116
116
 
117
117
  // If our 5% is lower than 80% of target we should drop quality
118
- if (currentFivePercent < 0.80 * targetFrameRate) {
118
+ if (currentFivePercent < 0.80 * Math.Min(targetFrameRate, 144)) {
119
119
  frameHealth = FrameHealth.Unhealthy;
120
120
  }
121
121
 
122
122
  // print($"[Quality Check] 5%: {currentFivePercent}, 0.5%: {pointFivePercent}, health: {frameHealth}");
123
123
 
124
124
  if (this.tracer != null) {
125
- this.tracer.SetMeasurement("fps_5_percent", currentFivePercent);
126
- this.tracer.SetMeasurement("fps_0.5_percent", pointFivePercent);
127
- this.tracer.SetMeasurement("target_fps", targetFrameRate);
125
+ this.tracer.SetMeasurement("spf_5_percent", 1f / Math.Max(0.01, currentFivePercent));
126
+ this.tracer.SetMeasurement("spf_0.5_percent", 1f / Math.Max(0.01, pointFivePercent));
127
+ this.tracer.SetMeasurement("target_spf", 1f / Math.Max(0.01, targetFrameRate));
128
128
  this.tracer.SetMeasurement("cpu_main_avg", avgFrameTimings.cpuMainAvg);
129
129
  this.tracer.SetMeasurement("cpu_render_avg", avgFrameTimings.cpuRenderAvg);
130
130
  this.tracer.SetMeasurement("gpu_avg", avgFrameTimings.gpuAvg);
@@ -6,35 +6,35 @@ using UnityEngine.EventSystems;
6
6
  public class CanvasUIEventInterceptor : MonoBehaviour {
7
7
 
8
8
  /** Generic pointer event. */
9
- public event Action<object, object, object> PointerEvent;
9
+ public event Action<int, int, int> PointerEvent;
10
10
 
11
11
  /** Generic hover event. */
12
- public event Action<object, object, object> HoverEvent;
12
+ public event Action<int, int, PointerEventData> HoverEvent;
13
13
 
14
14
  /** Params: InstanceId */
15
- public event Action<object> SubmitEvent;
15
+ public event Action<int> SubmitEvent;
16
16
 
17
17
  /** Params: InstanceId, string value */
18
- public event Action<object, object> InputFieldSubmitEvent;
18
+ public event Action<int, string> InputFieldSubmitEvent;
19
19
 
20
20
  /** Params: InstanceId */
21
- public event Action<object> SelectEvent;
21
+ public event Action<int> SelectEvent;
22
22
 
23
23
  /** Params: InstanceId */
24
- public event Action<object> DeselectEvent;
24
+ public event Action<int> DeselectEvent;
25
25
 
26
- public event Action<object> ClickEvent;
26
+ public event Action<int> ClickEvent;
27
27
 
28
- public event Action<object, object> ValueChangeEvent;
28
+ public event Action<int, float> ValueChangeEvent;
29
29
 
30
- public event Action<object, object> ToggleValueChangeEvent;
30
+ public event Action<int, bool> ToggleValueChangeEvent;
31
31
 
32
- public event Action<object, object> BeginDragEvent;
33
- public event Action<object, object> EndDragEvent;
34
- public event Action<object, object> DropEvent;
35
- public event Action<object, object> DragEvent;
32
+ public event Action<int, PointerEventData> BeginDragEvent;
33
+ public event Action<int, PointerEventData> EndDragEvent;
34
+ public event Action<int, PointerEventData> DropEvent;
35
+ public event Action<int, PointerEventData> DragEvent;
36
36
 
37
- public event Action<object, object> ScreenSizeChangeEvent;
37
+ public event Action<int, int> ScreenSizeChangeEvent;
38
38
 
39
39
  /** Fires a pointer event for instance that corresponds to `instanceId`. Includes pointer button and direction. (up or down) */
40
40
  public void FirePointerEvent(int instanceId, int direction, int button) {
@@ -3,6 +3,7 @@ using UnityEngine;
3
3
  using UnityEditor;
4
4
  #endif
5
5
 
6
+ [LuauAPI]
6
7
  [CreateAssetMenu(fileName = "VoxelBlockDefinition", menuName = "Airship/VoxelWorld/VoxelBlockDefinition")]
7
8
  public class VoxelBlockDefinition : ScriptableObject {
8
9
 
@@ -9,6 +9,7 @@ using System.IO;
9
9
  using UnityEditor;
10
10
  #endif
11
11
 
12
+ [LuauAPI]
12
13
  [CreateAssetMenu(fileName = "VoxelBlockDefinitionList", menuName = "Airship/VoxelWorld/VoxelBlockDefinitionList", order = 2)]
13
14
  public class VoxelBlockDefinitionList : ScriptableObject {
14
15
 
@@ -201,6 +201,7 @@ public class VoxelBlocks : MonoBehaviour {
201
201
  QuarterBlockTypes.DM, //UN
202
202
  };
203
203
 
204
+ [LuauAPI]
204
205
  public class LodSet {
205
206
  public VoxelMeshCopy lod0;
206
207
  public VoxelMeshCopy lod1;
@@ -208,6 +209,7 @@ public class VoxelBlocks : MonoBehaviour {
208
209
  }
209
210
 
210
211
  //The runtime version of VoxelBlockDefinition, after everything is loaded in
212
+ [LuauAPI]
211
213
  public class BlockDefinition {
212
214
  /// <summary>
213
215
  /// The generated world id for this block
@@ -1193,6 +1193,9 @@ namespace VoxelWorldStuff {
1193
1193
  handledBlockFaces[i] = new HashSet<int>();
1194
1194
  }
1195
1195
 
1196
+ // List used in internals of face expansion functions to avoid repeated non-main-thread allocations
1197
+ var expandTempList = new List<int>();
1198
+
1196
1199
  for (int x = 0; x < VoxelWorld.chunkSize; x++) {
1197
1200
  for (int y = 0; y < VoxelWorld.chunkSize; y++) {
1198
1201
  for (int z = 0; z < VoxelWorld.chunkSize; z++) {
@@ -1368,15 +1371,17 @@ namespace VoxelWorldStuff {
1368
1371
  var zScale = 1;
1369
1372
  // Only expand face in planes perpendicular to normal
1370
1373
  var flooredNorm = Vector3Int.FloorToInt(faceNormal);
1374
+ Profiler.BeginSample("MeshFace");
1371
1375
  if (Vector3.Dot(faceNormal, Vector3.right) == 0)
1372
1376
  while (ExpandFaceX(localVoxel, flooredNorm, faceIndex, blockIndex, damage,
1373
1377
  handledBlockFaces, xScale)) xScale++;
1374
1378
  if (Vector3.Dot(faceNormal, Vector3.forward) == 0)
1375
1379
  while (ExpandFaceZ(localVoxel, flooredNorm, faceIndex, blockIndex, damage,
1376
- handledBlockFaces, new Vector3(xScale, yScale, zScale), zScale)) zScale++;
1380
+ handledBlockFaces, expandTempList, new Vector3(xScale, yScale, zScale), zScale)) zScale++;
1377
1381
  if (Vector3.Dot(faceNormal, Vector3.up) == 0)
1378
1382
  while (ExpandFaceY(localVoxel, flooredNorm, faceIndex, blockIndex, damage,
1379
- handledBlockFaces, new Vector3(xScale, yScale, zScale), yScale)) yScale++;
1383
+ handledBlockFaces, expandTempList, new Vector3(xScale, yScale, zScale), yScale)) yScale++;
1384
+ Profiler.EndSample();
1380
1385
 
1381
1386
  Rect uvRect = block.GetUvsForFace(faceIndex);
1382
1387
 
@@ -1545,10 +1550,10 @@ namespace VoxelWorldStuff {
1545
1550
  return true;
1546
1551
  }
1547
1552
 
1548
- private bool ExpandFaceY(Vector3Int localVoxel, Vector3Int normal, int faceIndex, ushort blockIndex, float damage, HashSet<int>[] handledBlockFaces, Vector3 size, int y) {
1553
+ private bool ExpandFaceY(Vector3Int localVoxel, Vector3Int normal, int faceIndex, ushort blockIndex, float damage, HashSet<int>[] handledBlockFaces, List<int> expandChecks, Vector3 size, int y) {
1549
1554
  if (y + localVoxel.y >= chunkSize) return false;
1550
1555
 
1551
- var expandChecks = new List<int>();
1556
+ expandChecks.Clear();
1552
1557
  for (var x = 0; x < size.x; x++) {
1553
1558
  for (var z = 0; z < size.z; z++) {
1554
1559
  var expandCheck = (ushort) ((localVoxel.x + x) + (localVoxel.y + y) * paddedChunkSize +
@@ -1579,10 +1584,10 @@ namespace VoxelWorldStuff {
1579
1584
  return true;
1580
1585
  }
1581
1586
 
1582
- private bool ExpandFaceZ(Vector3Int localVoxel, Vector3Int normal, int faceIndex, ushort blockIndex, float damage, HashSet<int>[] handledBlockFaces, Vector3 size, int z) {
1587
+ private bool ExpandFaceZ(Vector3Int localVoxel, Vector3Int normal, int faceIndex, ushort blockIndex, float damage, HashSet<int>[] handledBlockFaces, List<int> expandChecks, Vector3 size, int z) {
1583
1588
  if (z + localVoxel.z >= chunkSize) return false;
1584
1589
 
1585
- var expandChecks = new List<int>();
1590
+ expandChecks.Clear();
1586
1591
  for (var x = 0; x < size.x; x++) {
1587
1592
  for (var y = 0; y < size.y; y++) {
1588
1593
  var expandCheck = (ushort) ((localVoxel.x + x) + (localVoxel.y + y) * paddedChunkSize +
@@ -1318,6 +1318,9 @@ public partial class VoxelWorld : MonoBehaviour {
1318
1318
  dist += 250f / chunkSize;
1319
1319
  }
1320
1320
  }
1321
+
1322
+ // Super prioritize priority updates
1323
+ // if (chunk.GetPriorityUpdate()) dist -= 1000;
1321
1324
 
1322
1325
  return dist;
1323
1326
  }
@@ -3,7 +3,7 @@
3
3
  <plist version="1.0">
4
4
  <dict>
5
5
  <key>BuildMachineOSBuild</key>
6
- <string>24G231</string>
6
+ <string>24G325</string>
7
7
  <key>CFBundleDevelopmentRegion</key>
8
8
  <string>en</string>
9
9
  <key>CFBundleExecutable</key>
@@ -27,19 +27,19 @@
27
27
  <key>DTCompiler</key>
28
28
  <string>com.apple.compilers.llvm.clang.1_0</string>
29
29
  <key>DTPlatformBuild</key>
30
- <string>25A352</string>
30
+ <string>25B74</string>
31
31
  <key>DTPlatformName</key>
32
32
  <string>macosx</string>
33
33
  <key>DTPlatformVersion</key>
34
- <string>26.0</string>
34
+ <string>26.1</string>
35
35
  <key>DTSDKBuild</key>
36
- <string>25A352</string>
36
+ <string>25B74</string>
37
37
  <key>DTSDKName</key>
38
- <string>macosx26.0</string>
38
+ <string>macosx26.1</string>
39
39
  <key>DTXcode</key>
40
- <string>2601</string>
40
+ <string>2611</string>
41
41
  <key>DTXcodeBuild</key>
42
- <string>17A400</string>
42
+ <string>17B100</string>
43
43
  <key>LSMinimumSystemVersion</key>
44
44
  <string>13.0</string>
45
45
  <key>NSHumanReadableCopyright</key>
Binary file
@@ -3426,7 +3426,7 @@ MonoBehaviour:
3426
3426
  m_Name:
3427
3427
  m_EditorClassIdentifier:
3428
3428
  wrap: {fileID: 6429755776191137293}
3429
- latency: 166
3429
+ latency: 100
3430
3430
  jitter: 0
3431
3431
  jitterSpeed: 1
3432
3432
  unreliableLoss: 0
@@ -1587,12 +1587,18 @@ namespace Mirror
1587
1587
  // (we add this to the UnityEngine in NetworkLoop)
1588
1588
  internal static void NetworkEarlyUpdate()
1589
1589
  {
1590
+ // EASYMOD: Moved above transport early update. UpdateTimeInterpolation() updates NetworkTime.time, so
1591
+ // you would have NetworkTime.time one frame behind when processing snapshot updates compared to when FixedUpdate()/Update() runs.
1592
+ // This throws off the localTimeline clock drift adjustment as it's operating on the previous NetworkTime.time when adjusting
1593
+ // based on the received time snapshot. This causes you to jump ahead of the expected time when the timeline advances.
1594
+ // If you had a low framerate, you may end up eating up all of the buffer time on this jump and even go ahead of the predicted server time, causing stuttering
1595
+ // for observed objects that depend on NetworkTime.time being buffered to give extra time for packets to arrive.
1596
+ // time snapshot interpolation
1597
+ UpdateTimeInterpolation();
1598
+
1590
1599
  // process all incoming messages first before updating the world
1591
1600
  if (Transport.active != null)
1592
1601
  Transport.active.ClientEarlyUpdate();
1593
-
1594
- // time snapshot interpolation
1595
- UpdateTimeInterpolation();
1596
1602
  }
1597
1603
 
1598
1604
  // NetworkLateUpdate called after any Update/FixedUpdate/LateUpdate
@@ -142,7 +142,7 @@ namespace Mirror
142
142
  snapshotSettings.catchupPositiveThreshold,
143
143
  ref deliveryTimeEma);
144
144
 
145
- // Debug.Log($"inserted TimeSnapshot remote={snap.remoteTime:F2} local={snap.localTime:F2} total={snapshots.Count}");
145
+ // Debug.Log($"inserted TimeSnapshot frame={Time.frameCount} remote={snap.remoteTime:F2} local={snap.localTime:F2} pred={localTimeline} diff={localTimeline - snap.remoteTime} total={snapshots.Count}");
146
146
  }
147
147
 
148
148
  // call this from early update, so the timeline is safe to use in update
@@ -37,6 +37,9 @@ namespace Mirror
37
37
  public double bufferTimeMultiplier = 2;
38
38
  public double bufferTime => NetworkServer.sendInterval * bufferTimeMultiplier;
39
39
 
40
+ // EASYMOD: This is a hack for us to be able to adjust lag comp requests per connection to account for the character input buffer. This is not used by anything in mirror.
41
+ public double inputBufferTime = 0;
42
+
40
43
  // <clienttime, snaps>
41
44
  readonly SortedList<double, TimeSnapshot> snapshots = new SortedList<double, TimeSnapshot>();
42
45
 
@@ -50,6 +53,9 @@ namespace Mirror
50
53
 
51
54
  /// <summary>Round trip time (in seconds) that it takes a message to go server->client->server.</summary>
52
55
  public double rtt => _rtt.Value;
56
+ /// <Summary>Round trip time variance aka jitter, in seconds.</Summary>
57
+ // "rttVariance" instead of "rttVar" for consistency with older versions.
58
+ public double rttVariance => _rtt.Variance;
53
59
 
54
60
  public NetworkConnectionToClient(int networkConnectionId, string clientAddress = "localhost")
55
61
  : base(networkConnectionId)
@@ -2016,13 +2016,15 @@ namespace Mirror
2016
2016
  fullUpdateDuration.Begin();
2017
2017
  }
2018
2018
 
2019
- // process all incoming messages first before updating the world
2020
- if (Transport.active != null)
2021
- Transport.active.ServerEarlyUpdate();
2022
-
2019
+ // EASYMOD: Moved ahead of transport early update for the same reason as
2020
+ // described in NetworkClient.NetworkEarlyUpdate()
2023
2021
  // step each connection's local time interpolation in early update.
2024
2022
  foreach (NetworkConnectionToClient connection in connections.Values)
2025
2023
  connection.UpdateTimeInterpolation();
2024
+
2025
+ // process all incoming messages first before updating the world
2026
+ if (Transport.active != null)
2027
+ Transport.active.ServerEarlyUpdate();
2026
2028
 
2027
2029
  if (active) earlyUpdateDuration.End();
2028
2030
  }
@@ -0,0 +1,42 @@
1
+ // Simple Moving Average (SMA) implementation
2
+ // Keeps a sliding window of the last n entries and drops older data
3
+ // https://en.wikipedia.org/wiki/Moving_average#Simple_moving_average
4
+ using System;
5
+ using System.Collections.Generic;
6
+
7
+ namespace Mirror
8
+ {
9
+ public class SimpleMovingAverage
10
+ {
11
+ readonly int maxSamples;
12
+ readonly Queue<double> samples;
13
+ double sum;
14
+
15
+ public double Value => samples.Count > 0 ? sum / samples.Count : 0;
16
+ public int Count => samples.Count;
17
+
18
+ public SimpleMovingAverage(int n)
19
+ {
20
+ maxSamples = n;
21
+ samples = new Queue<double>(n + 1);
22
+ sum = 0;
23
+ }
24
+
25
+ public void Add(double newValue)
26
+ {
27
+ samples.Enqueue(newValue);
28
+ sum += newValue;
29
+
30
+ if (samples.Count > maxSamples)
31
+ {
32
+ sum -= samples.Dequeue();
33
+ }
34
+ }
35
+
36
+ public void Reset()
37
+ {
38
+ samples.Clear();
39
+ sum = 0;
40
+ }
41
+ }
42
+ }
@@ -0,0 +1,3 @@
1
+ fileFormatVersion: 2
2
+ guid: 6f49e008a37547c2997bc58b2ec75034
3
+ timeCreated: 1764715854
@@ -19,7 +19,7 @@ MonoBehaviour:
19
19
  m_RendererDataList:
20
20
  - {fileID: 11400000, guid: fd1b4d11df5dd4a378ed9d33fdae2dbf, type: 2}
21
21
  m_DefaultRendererIndex: 0
22
- m_RequireDepthTexture: 0
22
+ m_RequireDepthTexture: 1
23
23
  m_RequireOpaqueTexture: 0
24
24
  m_OpaqueDownsampling: 1
25
25
  m_SupportsTerrainHoles: 1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gg.easy.airship",
3
- "version": "0.1.2194",
3
+ "version": "0.1.2195",
4
4
  "displayName": "Airship",
5
5
  "unity": "2021.3",
6
6
  "unityRelease": "12f1",