Compare commits
11 Commits
4871f7c150
...
93c55dd482
| Author | SHA1 | Date | |
|---|---|---|---|
| 93c55dd482 | |||
| 414ab90afc | |||
| 42a86419ca | |||
| 2140fc3868 | |||
| 4a2c73badb | |||
| d6c0ec2a33 | |||
| 634a5f7983 | |||
| 77f4d4fed0 | |||
| 3ee6fc4dc6 | |||
| e0b1b736c3 | |||
| 28e342b1e3 |
2
.gitignore
vendored
2
.gitignore
vendored
@@ -22,3 +22,5 @@ Desktop.ini
|
||||
**/obj/
|
||||
**/*.mdb
|
||||
**/*.pdb
|
||||
|
||||
/.claude
|
||||
47
AGENTS.md
47
AGENTS.md
@@ -1,47 +0,0 @@
|
||||
# Repository Guidelines
|
||||
|
||||
## Project Structure & Module Organization
|
||||
|
||||
- `Main.cs`: primary Harmony patches, gameplay hooks, and high-level multiplayer flow.
|
||||
- `Packets/`: network message types and handlers (client/server). Subfolders group by domain (e.g., `Lobby`, `Game`, `State`, `Handlers`).
|
||||
- `LoadSaveOverrides/`: multiplayer-aware save/load containers and BinaryFormatter binder.
|
||||
- `StateManagement/`: observer-based state syncing (e.g., building state updates).
|
||||
- `ServerBrowser/`, `ServerLobby/`, `UI/`: menu screens, lobby UI, and related scripts/prefabs glue.
|
||||
- `Riptide/`, `RiptideSteamTransport/`: networking and Steam transport integration.
|
||||
- `Enums/`, `Constants.cs`, `ErrorCodeMessages.cs`, `ReflectionHelper/`: shared types/utilities.
|
||||
|
||||
## Build, Test, and Development Commands
|
||||
|
||||
This mod is typically compiled/loaded by the game’s mod loader (there is no `.csproj` here).
|
||||
|
||||
- Validate changes quickly: `rg -n "TODO|FIXME|throw|NotImplementedException" -S .`
|
||||
- Inspect recent log output: `Get-Content .\\output.txt -Tail 200`
|
||||
- Check history/context: `git log -n 20 --oneline`
|
||||
|
||||
To run locally, copy/enable the mod in *Kingdoms and Castles* and **fully restart the game** after changes. Keep host/client mod versions identical.
|
||||
|
||||
## Coding Style & Naming Conventions
|
||||
|
||||
- Language: C# (Unity/Mono). Prefer conservative language features to avoid in-game compiler issues.
|
||||
- Indentation: 4 spaces; braces on new lines (match existing files).
|
||||
- Names: `PascalCase` for types/methods, `camelCase` for locals/fields. Packet properties are public and serialized—treat renames as breaking changes.
|
||||
- Logging: use `Main.helper.Log(...)` with short, searchable messages.
|
||||
|
||||
## Testing Guidelines
|
||||
|
||||
No automated test suite. Verify in-game with a minimal repro:
|
||||
|
||||
- Host ↔ join, place buildings, save/load, leave/rejoin, and confirm sync.
|
||||
- When reporting bugs, include `output.txt` excerpts around the first exception and “Save Transfer” markers.
|
||||
|
||||
## Commit & Pull Request Guidelines
|
||||
|
||||
Git history uses short, informal summaries. For contributions:
|
||||
|
||||
- Commits: one-line, descriptive, avoid profanity; include a scope when helpful (e.g., `save: fix load fallback`).
|
||||
- PRs: describe the issue, repro steps, expected vs actual, and attach relevant `output.txt` snippets. Note game version, mod version, and whether it’s Workshop or local mod folder.
|
||||
|
||||
## Agent-Specific Notes
|
||||
|
||||
- Avoid edits that depend on newer C# syntax not supported by the runtime compiler.
|
||||
- Prefer small, isolated fixes; multiplayer regressions are easy to introduce—add logs around save/load and connect/disconnect paths.
|
||||
@@ -47,6 +47,7 @@ namespace KCM.Enums
|
||||
PlaceKeepRandomly = 91,
|
||||
ResyncRequest = 92,
|
||||
ResourceSnapshot = 93,
|
||||
BuildingSnapshot = 94
|
||||
BuildingSnapshot = 94,
|
||||
VillagerSnapshot = 95
|
||||
}
|
||||
}
|
||||
|
||||
253
Main.cs
253
Main.cs
@@ -58,6 +58,7 @@ namespace KCM
|
||||
private static readonly Dictionary<int, long> lastTeamIdLookupLogMs = new Dictionary<int, long>();
|
||||
private static int resetInProgress = 0;
|
||||
private static int multiplayerSaveLoadInProgress = 0;
|
||||
private static int worldReadyRebuildDone = 0;
|
||||
|
||||
public static bool IsMultiplayerSaveLoadInProgress
|
||||
{
|
||||
@@ -94,6 +95,7 @@ namespace KCM
|
||||
|
||||
try { LobbyManager.loadingSave = false; } catch { }
|
||||
try { SetMultiplayerSaveLoadInProgress(false); } catch { }
|
||||
try { Interlocked.Exchange(ref worldReadyRebuildDone, 0); } catch { }
|
||||
|
||||
try
|
||||
{
|
||||
@@ -151,13 +153,108 @@ namespace KCM
|
||||
|
||||
try
|
||||
{
|
||||
FieldInfo loadTickDelayField = instance.GetType().GetField("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
FieldInfo loadTickDelayField = instance.GetType().GetField("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
if (loadTickDelayField != null)
|
||||
{
|
||||
loadTickDelayField.SetValue(instance, ticks);
|
||||
return;
|
||||
}
|
||||
|
||||
PropertyInfo loadTickDelayProp = instance.GetType().GetProperty("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
if (loadTickDelayProp != null && loadTickDelayProp.CanWrite && loadTickDelayProp.PropertyType == typeof(int))
|
||||
{
|
||||
loadTickDelayProp.SetValue(instance, ticks, null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Debug: list all fields if loadTickDelay not found
|
||||
if (instance.GetType().Name == "VillagerSystem")
|
||||
{
|
||||
helper?.Log("DEBUG: VillagerSystem fields:");
|
||||
foreach (var field in instance.GetType().GetFields(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public))
|
||||
{
|
||||
if (field.FieldType == typeof(int) || field.FieldType == typeof(bool))
|
||||
helper?.Log($" Field: {field.Name} ({field.FieldType.Name})");
|
||||
}
|
||||
helper?.Log("DEBUG: VillagerSystem properties:");
|
||||
foreach (var prop in instance.GetType().GetProperties(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public))
|
||||
{
|
||||
if (prop.PropertyType == typeof(int) || prop.PropertyType == typeof(bool))
|
||||
helper?.Log($" Property: {prop.Name} ({prop.PropertyType.Name})");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
helper?.Log("SetLoadTickDelay failed for " + instance.GetType().Name + ": " + e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static int GetLoadTickDelayOrMinusOne(object instance)
|
||||
{
|
||||
if (instance == null)
|
||||
return -1;
|
||||
|
||||
try
|
||||
{
|
||||
FieldInfo loadTickDelayField = instance.GetType().GetField("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
if (loadTickDelayField != null)
|
||||
{
|
||||
object v = loadTickDelayField.GetValue(instance);
|
||||
if (v is int)
|
||||
return (int)v;
|
||||
}
|
||||
|
||||
PropertyInfo loadTickDelayProp = instance.GetType().GetProperty("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public);
|
||||
if (loadTickDelayProp != null && loadTickDelayProp.CanRead && loadTickDelayProp.PropertyType == typeof(int))
|
||||
{
|
||||
object pv = loadTickDelayProp.GetValue(instance, null);
|
||||
if (pv is int)
|
||||
return (int)pv;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static string TryGetGameModeName()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (GameState.inst == null)
|
||||
return "null";
|
||||
|
||||
var t = GameState.inst.GetType();
|
||||
|
||||
var modeProp = t.GetProperty("mode", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
|
||||
?? t.GetProperty("Mode", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
|
||||
?? t.GetProperty("CurrentMode", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
|
||||
if (modeProp != null)
|
||||
{
|
||||
object m = modeProp.GetValue(GameState.inst, null);
|
||||
return m != null ? m.GetType().Name : "null";
|
||||
}
|
||||
|
||||
var modeField = t.GetField("mode", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
|
||||
?? t.GetField("Mode", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
|
||||
?? t.GetField("currentMode", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)
|
||||
?? t.GetField("currMode", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
|
||||
if (modeField != null)
|
||||
{
|
||||
object fm = modeField.GetValue(GameState.inst);
|
||||
return fm != null ? fm.GetType().Name : "null";
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
public static void RunPostLoadRebuild(string reason)
|
||||
@@ -171,10 +268,28 @@ namespace KCM
|
||||
try { Player.inst.irrigation.UpdateIrrigation(); } catch (Exception e) { helper?.Log(e.ToString()); }
|
||||
try { Player.inst.CalcMaxResources(null, -1); } catch (Exception e) { helper?.Log(e.ToString()); }
|
||||
|
||||
helper?.Log("Setting loadTickDelay for game systems");
|
||||
SetLoadTickDelay(Player.inst, 1);
|
||||
SetLoadTickDelay(UnitSystem.inst, 1);
|
||||
SetLoadTickDelay(JobSystem.inst, 1);
|
||||
SetLoadTickDelay(VillagerSystem.inst, 1);
|
||||
|
||||
helper?.Log(
|
||||
"loadTickDelay after set: Player=" + GetLoadTickDelayOrMinusOne(Player.inst) +
|
||||
" Unit=" + GetLoadTickDelayOrMinusOne(UnitSystem.inst) +
|
||||
" Job=" + GetLoadTickDelayOrMinusOne(JobSystem.inst) +
|
||||
" Villager=" + GetLoadTickDelayOrMinusOne(VillagerSystem.inst));
|
||||
|
||||
// Try to enable VillagerSystem if it's disabled
|
||||
if (VillagerSystem.inst != null && !VillagerSystem.inst.enabled)
|
||||
{
|
||||
helper?.Log("VillagerSystem is disabled, enabling it");
|
||||
VillagerSystem.inst.enabled = true;
|
||||
}
|
||||
else if (VillagerSystem.inst != null)
|
||||
{
|
||||
helper?.Log("VillagerSystem.enabled = " + VillagerSystem.inst.enabled);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
@@ -315,9 +430,123 @@ namespace KCM
|
||||
#endregion
|
||||
|
||||
public static int FixedUpdateInterval = 0;
|
||||
private static long lastVillagerMoveMs = 0;
|
||||
private static long lastVillagerProbeMs = 0;
|
||||
private static long lastVillagerStallLogMs = 0;
|
||||
private static Guid probedVillagerGuid = Guid.Empty;
|
||||
private static Vector3 probedVillagerLastPos = Vector3.zero;
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (KCClient.client != null &&
|
||||
KCClient.client.IsConnected &&
|
||||
Volatile.Read(ref worldReadyRebuildDone) == 0 &&
|
||||
World.inst != null &&
|
||||
Player.inst != null &&
|
||||
VillagerSystem.inst != null)
|
||||
{
|
||||
if (Interlocked.Exchange(ref worldReadyRebuildDone, 1) == 0)
|
||||
{
|
||||
Main.helper.Log("AutoRebuild: world ready; running post-load rebuild");
|
||||
RunPostLoadRebuild("auto:world-ready");
|
||||
Main.helper.Log(
|
||||
"AutoRebuild: timeScale=" + Time.timeScale +
|
||||
" loadTickDelay(Player/Unit/Job/Villager)=" +
|
||||
GetLoadTickDelayOrMinusOne(Player.inst) + "/" +
|
||||
GetLoadTickDelayOrMinusOne(UnitSystem.inst) + "/" +
|
||||
GetLoadTickDelayOrMinusOne(JobSystem.inst) + "/" +
|
||||
GetLoadTickDelayOrMinusOne(VillagerSystem.inst));
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (KCClient.client != null &&
|
||||
KCClient.client.IsConnected &&
|
||||
World.inst != null &&
|
||||
Time.timeScale > 0f &&
|
||||
Villager.villagers != null &&
|
||||
Villager.villagers.Count > 0)
|
||||
{
|
||||
long now = DateTimeOffset.Now.ToUnixTimeMilliseconds();
|
||||
if ((now - lastVillagerProbeMs) >= 2000)
|
||||
{
|
||||
lastVillagerProbeMs = now;
|
||||
|
||||
Villager v = null;
|
||||
try
|
||||
{
|
||||
if (probedVillagerGuid != Guid.Empty)
|
||||
v = Villager.villagers.data.FirstOrDefault(x => x != null && x.guid == probedVillagerGuid);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
if (v == null)
|
||||
{
|
||||
v = Villager.villagers.data.FirstOrDefault(x => x != null);
|
||||
if (v != null)
|
||||
{
|
||||
probedVillagerGuid = v.guid;
|
||||
probedVillagerLastPos = v.Pos;
|
||||
lastVillagerMoveMs = now;
|
||||
}
|
||||
}
|
||||
|
||||
if (v != null)
|
||||
{
|
||||
float movedSqr = (v.Pos - probedVillagerLastPos).sqrMagnitude;
|
||||
if (movedSqr > 0.01f)
|
||||
{
|
||||
probedVillagerLastPos = v.Pos;
|
||||
lastVillagerMoveMs = now;
|
||||
}
|
||||
|
||||
if ((now - lastVillagerMoveMs) >= 15000 && (now - lastVillagerStallLogMs) >= 15000)
|
||||
{
|
||||
lastVillagerStallLogMs = now;
|
||||
Main.helper.Log(
|
||||
"VillagerStallDetect: no movement for " + (now - lastVillagerMoveMs) +
|
||||
"ms timeScale=" + Time.timeScale +
|
||||
" mode=" + TryGetGameModeName() +
|
||||
" villagerSystemEnabled=" + (VillagerSystem.inst != null && VillagerSystem.inst.enabled) +
|
||||
" villagers=" + Villager.villagers.Count +
|
||||
" sampleGuid=" + probedVillagerGuid +
|
||||
" samplePos=" + v.Pos);
|
||||
Main.helper.Log(
|
||||
"VillagerStallDetect: loadTickDelay(Player/Unit/Job/Villager)=" +
|
||||
GetLoadTickDelayOrMinusOne(Player.inst) + "/" +
|
||||
GetLoadTickDelayOrMinusOne(UnitSystem.inst) + "/" +
|
||||
GetLoadTickDelayOrMinusOne(JobSystem.inst) + "/" +
|
||||
GetLoadTickDelayOrMinusOne(VillagerSystem.inst));
|
||||
|
||||
// Try to fix stalled systems by resetting loadTickDelay
|
||||
if (GetLoadTickDelayOrMinusOne(VillagerSystem.inst) == -1 ||
|
||||
GetLoadTickDelayOrMinusOne(UnitSystem.inst) == -1 ||
|
||||
GetLoadTickDelayOrMinusOne(JobSystem.inst) == -1)
|
||||
{
|
||||
Main.helper.Log("VillagerStallDetect: Attempting to fix stalled systems");
|
||||
SetLoadTickDelay(Player.inst, 1);
|
||||
SetLoadTickDelay(UnitSystem.inst, 1);
|
||||
SetLoadTickDelay(JobSystem.inst, 1);
|
||||
SetLoadTickDelay(VillagerSystem.inst, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
// send batched building placement info
|
||||
/*if (PlaceHook.QueuedBuildings.Count > 0 && (FixedUpdateInterval % 25 == 0))
|
||||
{
|
||||
@@ -1156,8 +1385,26 @@ namespace KCM
|
||||
Main.helper.Log($"set speed Called by 2: {new StackFrame(2).GetMethod()} {new StackFrame(2).GetMethod().Name.Contains("HandlePacket")}");
|
||||
Main.helper.Log($"set speed Called by 3: {new StackFrame(3).GetMethod()} {new StackFrame(3).GetMethod().Name.Contains("HandlePacket")}");*/
|
||||
|
||||
if (new StackFrame(3).GetMethod().Name.Contains("HandlePacket"))
|
||||
return;
|
||||
try
|
||||
{
|
||||
if (new StackFrame(3).GetMethod().Name.Contains("HandlePacket"))
|
||||
return;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (idx > 0 && Time.timeScale == 0f)
|
||||
{
|
||||
Time.timeScale = 1f;
|
||||
Main.helper.Log("TimeScaleFix: restored Time.timeScale=1 on local SetSpeed idx=" + idx);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
Main.helper.Log("SpeedControlUI.SetSpeed (local): " + idx);
|
||||
bool isPaused = (idx == 0);
|
||||
|
||||
56
Packets/Game/GameVillager/VillagerSnapshotPacket.cs
Normal file
56
Packets/Game/GameVillager/VillagerSnapshotPacket.cs
Normal file
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace KCM.Packets.Game.GameVillager
|
||||
{
|
||||
public class VillagerSnapshotPacket : Packet
|
||||
{
|
||||
public override ushort packetId => (ushort)Enums.Packets.VillagerSnapshot;
|
||||
|
||||
public List<Guid> guids { get; set; } = new List<Guid>();
|
||||
public List<Vector3> positions { get; set; } = new List<Vector3>();
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
if (KCClient.client != null && clientId == KCClient.client.Id)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
int count = Math.Min(guids?.Count ?? 0, positions?.Count ?? 0);
|
||||
if (count == 0)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Guid guid = guids[i];
|
||||
Vector3 position = positions[i];
|
||||
|
||||
Villager villager = null;
|
||||
try
|
||||
{
|
||||
villager = Villager.villagers?.data.FirstOrDefault(v => v != null && v.guid == guid);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
if (villager == null)
|
||||
continue;
|
||||
|
||||
villager.TeleportTo(position);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Main.helper.Log("Error handling villager snapshot packet: " + e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -310,6 +310,27 @@ namespace KCM.Packets.Handlers
|
||||
message.AddInt(item);
|
||||
}
|
||||
|
||||
else if (prop.PropertyType == typeof(List<Guid>))
|
||||
{
|
||||
currentPropName = prop.Name;
|
||||
List<Guid> list = (List<Guid>)prop.GetValue(packet, null);
|
||||
message.AddInt(list.Count);
|
||||
foreach (var item in list)
|
||||
message.AddBytes(item.ToByteArray(), true);
|
||||
}
|
||||
else if (prop.PropertyType == typeof(List<Vector3>))
|
||||
{
|
||||
currentPropName = prop.Name;
|
||||
List<Vector3> list = (List<Vector3>)prop.GetValue(packet, null);
|
||||
message.AddInt(list.Count);
|
||||
foreach (var item in list)
|
||||
{
|
||||
message.AddFloat(item.x);
|
||||
message.AddFloat(item.y);
|
||||
message.AddFloat(item.z);
|
||||
}
|
||||
}
|
||||
|
||||
else if (prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
|
||||
{
|
||||
currentPropName = prop.Name;
|
||||
@@ -543,6 +564,29 @@ namespace KCM.Packets.Handlers
|
||||
|
||||
prop.SetValue(p, list);
|
||||
}
|
||||
else if (prop.PropertyType == typeof(List<Guid>))
|
||||
{
|
||||
int count = message.GetInt();
|
||||
List<Guid> list = new List<Guid>();
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
list.Add(new Guid(message.GetBytes()));
|
||||
|
||||
prop.SetValue(p, list);
|
||||
}
|
||||
else if (prop.PropertyType == typeof(List<Vector3>))
|
||||
{
|
||||
int count = message.GetInt();
|
||||
List<Vector3> list = new List<Vector3>();
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Vector3 vector = new Vector3(message.GetFloat(), message.GetFloat(), message.GetFloat());
|
||||
list.Add(vector);
|
||||
}
|
||||
|
||||
prop.SetValue(p, list);
|
||||
}
|
||||
else if (prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
|
||||
{
|
||||
IDictionary dictionary = (IDictionary)prop.GetValue(p, null);
|
||||
|
||||
@@ -39,6 +39,25 @@ namespace KCM.Packets.Lobby
|
||||
Main.helper.Log(ex.ToString());
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
GameState.inst.SetNewMode(GameState.inst.playingMode);
|
||||
Main.helper.Log("StartGame: forced playing mode");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("StartGame: failed forcing playing mode");
|
||||
Main.helper.Log(ex.ToString());
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Main.RunPostLoadRebuild("StartGame");
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
SpeedControlUI.inst.SetSpeed(0);
|
||||
}
|
||||
else
|
||||
|
||||
@@ -16,9 +16,11 @@ namespace KCM.StateManagement.Sync
|
||||
private const int MaxBuildingSnapshotBytes = 30000;
|
||||
private const int MaxVillagerTeleportsPerResync = 400;
|
||||
private const int VillagerValidationIntervalMs = 10000; // 10 seconds
|
||||
private const int VillagerSnapshotIntervalMs = 1000;
|
||||
|
||||
private static long lastResourceBroadcastMs;
|
||||
private static long lastVillagerValidationMs;
|
||||
private static long lastVillagerSnapshotMs;
|
||||
|
||||
private static FieldInfo freeResourceAmountField;
|
||||
private static MethodInfo resourceAmountGetMethod;
|
||||
@@ -63,6 +65,20 @@ namespace KCM.StateManagement.Sync
|
||||
lastVillagerValidationMs = now;
|
||||
ValidateAndCorrectVillagerStates();
|
||||
}
|
||||
|
||||
if ((now - lastVillagerSnapshotMs) >= VillagerSnapshotIntervalMs)
|
||||
{
|
||||
lastVillagerSnapshotMs = now;
|
||||
try
|
||||
{
|
||||
BroadcastVillagerSnapshot();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("Error broadcasting villager snapshot");
|
||||
Main.helper.Log(ex.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void SendResyncToClient(ushort toClient, string reason)
|
||||
@@ -559,6 +575,44 @@ namespace KCM.StateManagement.Sync
|
||||
}
|
||||
}
|
||||
|
||||
private static void BroadcastVillagerSnapshot()
|
||||
{
|
||||
if (!KCServer.IsRunning)
|
||||
return;
|
||||
|
||||
if (KCServer.server.ClientCount == 0)
|
||||
return;
|
||||
|
||||
if (Villager.villagers == null || Villager.villagers.Count == 0)
|
||||
return;
|
||||
|
||||
List<Guid> guids = new List<Guid>();
|
||||
List<Vector3> positions = new List<Vector3>();
|
||||
const int maxVillagersPerSnapshot = 50;
|
||||
|
||||
for (int i = 0; i < Villager.villagers.Count && guids.Count < maxVillagersPerSnapshot; i++)
|
||||
{
|
||||
Villager villager = Villager.villagers.data[i];
|
||||
if (villager == null)
|
||||
continue;
|
||||
|
||||
guids.Add(villager.guid);
|
||||
positions.Add(villager.Pos);
|
||||
}
|
||||
|
||||
if (guids.Count == 0)
|
||||
return;
|
||||
|
||||
VillagerSnapshotPacket snapshot = new VillagerSnapshotPacket
|
||||
{
|
||||
guids = guids,
|
||||
positions = positions
|
||||
};
|
||||
|
||||
ushort exceptId = KCClient.client != null ? KCClient.client.Id : (ushort)0;
|
||||
snapshot.SendToAll(exceptId);
|
||||
}
|
||||
|
||||
public static void ApplyResourceSnapshot(List<int> resourceTypes, List<int> amounts)
|
||||
{
|
||||
if (resourceTypes == null || amounts == null)
|
||||
|
||||
Reference in New Issue
Block a user