Compare commits
8 Commits
main
...
b6bcde41b1
| Author | SHA1 | Date | |
|---|---|---|---|
| b6bcde41b1 | |||
| 9b5fb2c632 | |||
| cf76acccf3 | |||
| 9868d30810 | |||
| 5abd025860 | |||
| d7718c1dff | |||
| ea57e0a52c | |||
| c3223d5db9 |
@@ -1,12 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(wc:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(tail:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -1,5 +1,7 @@
|
||||
# Logs / local debug output
|
||||
output*.txt
|
||||
*.log
|
||||
/log.txt
|
||||
|
||||
# OS junk
|
||||
.DS_Store
|
||||
@@ -20,8 +22,3 @@ Desktop.ini
|
||||
**/obj/
|
||||
**/*.mdb
|
||||
**/*.pdb
|
||||
|
||||
/.claude
|
||||
/*.png
|
||||
/*.txt
|
||||
/*.jpg
|
||||
|
||||
47
AGENTS.md
Normal file
47
AGENTS.md
Normal file
@@ -0,0 +1,47 @@
|
||||
# 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.
|
||||
10
Attributes/NoServerRelayAttribute.cs
Normal file
10
Attributes/NoServerRelayAttribute.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System;
|
||||
|
||||
namespace KCM.Attributes
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
|
||||
public class NoServerRelayAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
15
Constants.cs
15
Constants.cs
@@ -15,20 +15,19 @@ namespace KCM
|
||||
/// </summary>
|
||||
public static class Constants
|
||||
{
|
||||
// Use lazy initialization to avoid null reference when GameState isn't ready yet
|
||||
public static MainMenuMode MainMenuMode => GameState.inst?.mainMenuMode;
|
||||
public static PlayingMode PlayingMode => GameState.inst?.playingMode;
|
||||
public static World World => GameState.inst?.world;
|
||||
public static readonly MainMenuMode MainMenuMode = GameState.inst.mainMenuMode;
|
||||
public static readonly PlayingMode PlayingMode = GameState.inst.playingMode;
|
||||
public static readonly World World = GameState.inst.world;
|
||||
|
||||
#region "UI"
|
||||
public static Transform MainMenuUI_T => MainMenuMode?.mainMenuUI?.transform;
|
||||
public static GameObject MainMenuUI_O => MainMenuMode?.mainMenuUI;
|
||||
public static readonly Transform MainMenuUI_T = MainMenuMode.mainMenuUI.transform;
|
||||
public static readonly GameObject MainMenuUI_O = MainMenuMode.mainMenuUI;
|
||||
|
||||
/* public static readonly Transform TopLevelUI_T = MainMenuUI_T.parent;
|
||||
public static readonly GameObject TopLevelUI_O = MainMenuUI_T.parent.gameObject;*/
|
||||
|
||||
public static Transform ChooseModeUI_T => MainMenuMode?.chooseModeUI?.transform;
|
||||
public static GameObject ChooseModeUI_O => MainMenuMode?.chooseModeUI;
|
||||
public static readonly Transform ChooseModeUI_T = MainMenuMode.chooseModeUI.transform;
|
||||
public static readonly GameObject ChooseModeUI_O = MainMenuMode.chooseModeUI;
|
||||
#endregion
|
||||
|
||||
}
|
||||
|
||||
@@ -18,8 +18,11 @@ namespace KCM.Enums
|
||||
KingdomName = 32,
|
||||
StartGame = 33,
|
||||
WorldSeed = 34,
|
||||
|
||||
|
||||
Building = 50,
|
||||
BuildingOnPlacement = 51,
|
||||
|
||||
World = 70,
|
||||
WorldPlace = 71,
|
||||
FellTree = 72,
|
||||
@@ -42,6 +45,8 @@ namespace KCM.Enums
|
||||
SetupInitialWorkers = 89,
|
||||
VillagerTeleportTo = 90,
|
||||
PlaceKeepRandomly = 91,
|
||||
BuildingRemove = 92
|
||||
ResyncRequest = 92,
|
||||
ResourceSnapshot = 93,
|
||||
BuildingSnapshot = 94
|
||||
}
|
||||
}
|
||||
|
||||
27
KCClient.cs
27
KCClient.cs
@@ -19,7 +19,7 @@ namespace KCM
|
||||
{
|
||||
public class KCClient : MonoBehaviour
|
||||
{
|
||||
public static Client client = new Client(Main.steamClient);
|
||||
public static Client client = new Client();
|
||||
|
||||
public string Name { get; set; }
|
||||
|
||||
@@ -36,10 +36,11 @@ namespace KCM
|
||||
|
||||
private static void Client_Disconnected(object sender, DisconnectedEventArgs e)
|
||||
{
|
||||
Main.CleanupMultiplayerSession();
|
||||
Main.helper.Log("Client disconnected event start");
|
||||
try
|
||||
{
|
||||
Main.ResetMultiplayerState("Client disconnected");
|
||||
|
||||
if (e.Message != null)
|
||||
{
|
||||
Main.helper.Log(e.Message.ToString());
|
||||
@@ -77,7 +78,19 @@ namespace KCM
|
||||
|
||||
private static void Client_Connected(object sender, EventArgs e)
|
||||
{
|
||||
|
||||
try
|
||||
{
|
||||
if (client != null && client.Connection != null)
|
||||
{
|
||||
client.Connection.CanQualityDisconnect = false;
|
||||
client.Connection.MaxSendAttempts = 50;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("Error configuring client connection");
|
||||
Main.helper.Log(ex.ToString());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -88,7 +101,8 @@ namespace KCM
|
||||
|
||||
public static void Connect(string ip)
|
||||
{
|
||||
Main.helper.Log("Trying to connect to: " + ip);
|
||||
Main.EnsureNetworking();
|
||||
Main.Log("Trying to connect to: " + ip);
|
||||
client.Connect(ip, useMessageHandlers: false);
|
||||
}
|
||||
|
||||
@@ -99,8 +113,9 @@ namespace KCM
|
||||
|
||||
private void Preload(KCModHelper helper)
|
||||
{
|
||||
|
||||
helper.Log("Preload run in client");
|
||||
Main.helper = helper;
|
||||
Main.EnsureNetworking();
|
||||
Main.Log("Preload run in client");
|
||||
}
|
||||
|
||||
private void SceneLoaded(KCModHelper helper)
|
||||
|
||||
116
KCServer.cs
116
KCServer.cs
@@ -18,9 +18,12 @@ namespace KCM
|
||||
{
|
||||
public class KCServer : MonoBehaviour
|
||||
{
|
||||
public static Server server = new Server(Main.steamServer);
|
||||
public static Server server = new Server();
|
||||
public static bool started = false;
|
||||
|
||||
private static readonly Dictionary<ushort, Queue<SaveTransferPacket>> saveTransferQueues = new Dictionary<ushort, Queue<SaveTransferPacket>>();
|
||||
private const int SaveTransferPacketsPerUpdatePerClient = 10;
|
||||
|
||||
static KCServer()
|
||||
{
|
||||
//server.registerMessageHandler(typeof(KCServer).GetMethod("ClientJoined"));
|
||||
@@ -30,6 +33,7 @@ namespace KCM
|
||||
|
||||
public static void StartServer()
|
||||
{
|
||||
Main.EnsureNetworking();
|
||||
server = new Server(Main.steamServer);
|
||||
server.MessageReceived += PacketHandler.HandlePacketServer;
|
||||
|
||||
@@ -50,6 +54,7 @@ namespace KCM
|
||||
}
|
||||
|
||||
ev.Client.CanQualityDisconnect = false;
|
||||
ev.Client.MaxSendAttempts = 50;
|
||||
|
||||
Main.helper.Log("Client ID is: " + ev.Client.Id);
|
||||
|
||||
@@ -58,15 +63,42 @@ namespace KCM
|
||||
|
||||
server.ClientDisconnected += (obj, ev) =>
|
||||
{
|
||||
new ChatSystemMessage()
|
||||
try
|
||||
{
|
||||
Message = $"{Main.GetPlayerByClientID(ev.Client.Id).name} has left the server.",
|
||||
}.SendToAll();
|
||||
var playerName = $"Client {ev.Client.Id}";
|
||||
string steamId;
|
||||
if (Main.clientSteamIds.TryGetValue(ev.Client.Id, out steamId) && !string.IsNullOrEmpty(steamId))
|
||||
{
|
||||
KCPlayer player;
|
||||
if (Main.kCPlayers.TryGetValue(steamId, out player) && player != null && !string.IsNullOrEmpty(player.name))
|
||||
playerName = player.name;
|
||||
|
||||
Main.kCPlayers.Remove(Main.GetPlayerByClientID(ev.Client.Id).steamId);
|
||||
Destroy(LobbyHandler.playerEntries.Select(x => x.GetComponent<PlayerEntryScript>()).Where(x => x.Client == ev.Client.Id).FirstOrDefault().gameObject);
|
||||
Main.kCPlayers.Remove(steamId);
|
||||
}
|
||||
|
||||
Main.helper.Log($"Client disconnected. {ev.Reason}");
|
||||
Main.clientSteamIds.Remove(ev.Client.Id);
|
||||
|
||||
new ChatSystemMessage()
|
||||
{
|
||||
Message = $"{playerName} has left the server.",
|
||||
}.SendToAll();
|
||||
|
||||
var entry = LobbyHandler.playerEntries
|
||||
.Select(x => x != null ? x.GetComponent<PlayerEntryScript>() : null)
|
||||
.FirstOrDefault(x => x != null && x.Client == ev.Client.Id);
|
||||
|
||||
if (entry != null)
|
||||
Destroy(entry.gameObject);
|
||||
|
||||
saveTransferQueues.Remove(ev.Client.Id);
|
||||
|
||||
Main.helper.Log($"Client disconnected. {ev.Reason}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("Error handling client disconnect");
|
||||
Main.helper.Log(ex.ToString());
|
||||
}
|
||||
};
|
||||
|
||||
Main.helper.Log($"Listening on port 7777. Max {LobbyHandler.ServerSettings.MaxPlayers} clients.");
|
||||
@@ -100,6 +132,69 @@ namespace KCM
|
||||
private void Update()
|
||||
{
|
||||
server.Update();
|
||||
ProcessSaveTransfers();
|
||||
KCM.StateManagement.Sync.SyncManager.ServerUpdate();
|
||||
}
|
||||
|
||||
private static void ProcessSaveTransfers()
|
||||
{
|
||||
if (!KCServer.IsRunning)
|
||||
return;
|
||||
|
||||
if (saveTransferQueues.Count == 0)
|
||||
return;
|
||||
|
||||
var clients = saveTransferQueues.Keys.ToList();
|
||||
foreach (var clientId in clients)
|
||||
{
|
||||
Queue<SaveTransferPacket> queue;
|
||||
if (!saveTransferQueues.TryGetValue(clientId, out queue) || queue == null)
|
||||
continue;
|
||||
|
||||
int sentThisUpdate = 0;
|
||||
while (sentThisUpdate < SaveTransferPacketsPerUpdatePerClient && queue.Count > 0)
|
||||
{
|
||||
var packet = queue.Dequeue();
|
||||
packet.Send(clientId);
|
||||
sentThisUpdate++;
|
||||
}
|
||||
|
||||
if (queue.Count == 0)
|
||||
saveTransferQueues.Remove(clientId);
|
||||
}
|
||||
}
|
||||
|
||||
public static void EnqueueSaveTransfer(ushort toClient, byte[] bytes)
|
||||
{
|
||||
if (bytes == null)
|
||||
return;
|
||||
|
||||
int chunkSize = 900;
|
||||
int sent = 0;
|
||||
int totalChunks = (int)Math.Ceiling((double)bytes.Length / chunkSize);
|
||||
|
||||
var queue = new Queue<SaveTransferPacket>(totalChunks);
|
||||
for (int i = 0; i < totalChunks; i++)
|
||||
{
|
||||
int currentChunkSize = Math.Min(chunkSize, bytes.Length - sent);
|
||||
var chunk = new byte[currentChunkSize];
|
||||
Array.Copy(bytes, sent, chunk, 0, currentChunkSize);
|
||||
|
||||
queue.Enqueue(new SaveTransferPacket()
|
||||
{
|
||||
saveSize = bytes.Length,
|
||||
saveDataChunk = chunk,
|
||||
chunkId = i,
|
||||
chunkSize = chunk.Length,
|
||||
saveDataIndex = sent,
|
||||
totalChunks = totalChunks
|
||||
});
|
||||
|
||||
sent += currentChunkSize;
|
||||
}
|
||||
|
||||
saveTransferQueues[toClient] = queue;
|
||||
Main.helper.Log($"Queued {totalChunks} save data chunks for client {toClient}");
|
||||
}
|
||||
|
||||
private void OnApplicationQuit()
|
||||
@@ -109,9 +204,10 @@ namespace KCM
|
||||
|
||||
private void Preload(KCModHelper helper)
|
||||
{
|
||||
helper.Log("server?");
|
||||
|
||||
helper.Log("Preload run in server");
|
||||
Main.helper = helper;
|
||||
Main.EnsureNetworking();
|
||||
Main.Log("server?");
|
||||
Main.Log("Preload run in server");
|
||||
}
|
||||
|
||||
private void SceneLoaded(KCModHelper helper)
|
||||
|
||||
79
LoadSaveOverrides/LoadSaveLoadHooks.cs
Normal file
79
LoadSaveOverrides/LoadSaveLoadHooks.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using Harmony;
|
||||
using KCM.LoadSaveOverrides;
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
namespace KCM
|
||||
{
|
||||
public static class LoadSaveLoadAtPathHook
|
||||
{
|
||||
public static byte[] saveData = new byte[1];
|
||||
public static string lastLoadedPath = string.Empty;
|
||||
}
|
||||
|
||||
public static class LoadSaveLoadHook
|
||||
{
|
||||
public static byte[] saveBytes = null;
|
||||
public static bool memoryStreamHook = false;
|
||||
public static MultiplayerSaveContainer saveContainer = null;
|
||||
}
|
||||
|
||||
[HarmonyPatch(typeof(LoadSave), "LoadAtPath")]
|
||||
public class LoadSaveLoadAtPathCaptureHook
|
||||
{
|
||||
public static void Prefix(string path)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrEmpty(path))
|
||||
return;
|
||||
|
||||
LoadSaveLoadAtPathHook.lastLoadedPath = path;
|
||||
|
||||
if (!FileExists(path))
|
||||
return;
|
||||
|
||||
LoadSaveLoadAtPathHook.saveData = ReadAllBytes(path);
|
||||
Main.Log($"Captured save bytes from: {path} ({LoadSaveLoadAtPathHook.saveData.Length} bytes)");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.Log("Failed capturing save bytes from LoadSave.LoadAtPath");
|
||||
Main.Log(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool FileExists(string path)
|
||||
{
|
||||
object result = InvokeSystemIoFile("Exists", new Type[] { typeof(string) }, new object[] { path });
|
||||
return result is bool && (bool)result;
|
||||
}
|
||||
|
||||
private static byte[] ReadAllBytes(string path)
|
||||
{
|
||||
object result = InvokeSystemIoFile("ReadAllBytes", new Type[] { typeof(string) }, new object[] { path });
|
||||
return result as byte[];
|
||||
}
|
||||
|
||||
private static object InvokeSystemIoFile(string methodName, Type[] parameterTypes, object[] args)
|
||||
{
|
||||
// Avoid direct references to System.IO in IL (some mod loaders forbid it).
|
||||
const string typeName = "System.IO.File";
|
||||
Type fileType =
|
||||
Type.GetType(typeName) ??
|
||||
Type.GetType(typeName + ", mscorlib") ??
|
||||
Type.GetType(typeName + ", System") ??
|
||||
Type.GetType(typeName + ", System.Runtime") ??
|
||||
Type.GetType(typeName + ", System.Private.CoreLib");
|
||||
|
||||
if (fileType == null)
|
||||
return null;
|
||||
|
||||
var method = fileType.GetMethod(methodName, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Static, null, parameterTypes, null);
|
||||
if (method == null)
|
||||
return null;
|
||||
|
||||
return method.Invoke(null, args);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using Assets.Code;
|
||||
using Assets.Code;
|
||||
using Riptide;
|
||||
using Riptide.Transports;
|
||||
using Steamworks;
|
||||
@@ -8,6 +8,7 @@ using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace KCM.LoadSaveOverrides
|
||||
{
|
||||
@@ -24,15 +25,41 @@ namespace KCM.LoadSaveOverrides
|
||||
|
||||
Main.helper.Log($"Saving data for {Main.kCPlayers.Count} ({KCServer.server.ClientCount}) players.");
|
||||
|
||||
//this.PlayerSaveData = new PlayerSaveDataOverride().Pack(Player.inst);
|
||||
foreach (var player in Main.kCPlayers.Values)
|
||||
{
|
||||
Main.helper.Log($"Attempting to pack data for: " + player.name + $"({player.steamId})");
|
||||
Main.helper.Log($"{player.inst.ToString()} {player.inst?.gameObject.name}");
|
||||
this.players.Add(player.steamId, new Player.PlayerSaveData().Pack(player.inst));
|
||||
kingdomNames.Add(player.steamId, player.kingdomName);
|
||||
try
|
||||
{
|
||||
if (player == null)
|
||||
continue;
|
||||
|
||||
Main.helper.Log($"{players[player.steamId] == null}");
|
||||
if (string.IsNullOrWhiteSpace(player.steamId))
|
||||
{
|
||||
Main.helper.Log($"Skipping save for player with missing steamId (name={player.name ?? string.Empty})");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (player.inst == null)
|
||||
{
|
||||
Main.helper.Log($"Skipping save for player {player.name ?? string.Empty} ({player.steamId}) because Player.inst is null");
|
||||
continue;
|
||||
}
|
||||
|
||||
Main.helper.Log($"Attempting to pack data for: {player.name} ({player.steamId})");
|
||||
string playerGoName = (player.inst.gameObject != null) ? player.inst.gameObject.name : string.Empty;
|
||||
Main.helper.Log($"Player object: {player.inst} {playerGoName}");
|
||||
|
||||
this.players[player.steamId] = new Player.PlayerSaveData().Pack(player.inst);
|
||||
kingdomNames[player.steamId] = player.kingdomName ?? " ";
|
||||
|
||||
Main.helper.Log($"{players[player.steamId] == null}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
string steamId = (player != null && player.steamId != null) ? player.steamId : string.Empty;
|
||||
string name = (player != null && player.name != null) ? player.name : string.Empty;
|
||||
Main.helper.Log($"Error packing player data for save (steamId={steamId}, name={name})");
|
||||
Main.helper.Log(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
this.WorldSaveData = new World.WorldSaveData().Pack(World.inst);
|
||||
@@ -44,7 +71,25 @@ namespace KCM.LoadSaveOverrides
|
||||
this.DragonSpawnSaveData = new DragonSpawn.DragonSpawnSaveData().Pack(DragonSpawn.inst);
|
||||
this.UnitSystemSaveData = new UnitSystem.UnitSystemSaveData().Pack(UnitSystem.inst);
|
||||
this.RaidSystemSaveData2 = new RaiderSystem.RaiderSystemSaveData2().Pack(RaiderSystem.inst);
|
||||
this.ShipSystemSaveData = new ShipSystem.ShipSystemSaveData().Pack(ShipSystem.inst);
|
||||
|
||||
if (ShipSystem.inst != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
this.ShipSystemSaveData = new ShipSystem.ShipSystemSaveData().Pack(ShipSystem.inst);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("Error packing ShipSystem for save; skipping ShipSystemSaveData.");
|
||||
Main.helper.Log(ex.ToString());
|
||||
this.ShipSystemSaveData = null;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
this.ShipSystemSaveData = null;
|
||||
}
|
||||
|
||||
this.AIBrainsSaveData = new AIBrainsContainer.SaveData().Pack(AIBrainsContainer.inst);
|
||||
this.SiegeMonsterSaveData = new SiegeMonster.SiegeMonsterSaveData().Pack(null);
|
||||
this.CartSystemSaveData = new CartSystem.CartSystemSaveData().Pack(CartSystem.inst);
|
||||
@@ -57,10 +102,8 @@ namespace KCM.LoadSaveOverrides
|
||||
|
||||
public override object Unpack(object obj)
|
||||
{
|
||||
//original Player reset was up here
|
||||
foreach (var kvp in players)
|
||||
{
|
||||
|
||||
KCPlayer player;
|
||||
|
||||
if (!Main.kCPlayers.TryGetValue(kvp.Key, out player))
|
||||
@@ -75,7 +118,6 @@ namespace KCM.LoadSaveOverrides
|
||||
foreach (var player in Main.kCPlayers.Values)
|
||||
player.inst.Reset();
|
||||
|
||||
|
||||
AIBrainsContainer.inst.ClearAIs();
|
||||
this.CameraSaveData.Unpack(Cam.inst);
|
||||
this.WorldSaveData.Unpack(World.inst);
|
||||
@@ -87,10 +129,6 @@ namespace KCM.LoadSaveOverrides
|
||||
}
|
||||
this.TownNameSaveData.Unpack(TownNameUI.inst);
|
||||
|
||||
|
||||
//TownNameUI.inst.townName = kingdomNames[Main.PlayerSteamID];
|
||||
TownNameUI.inst.SetTownName(kingdomNames[Main.PlayerSteamID]);
|
||||
|
||||
Main.helper.Log("Unpacking player data");
|
||||
|
||||
Player.PlayerSaveData clientPlayerData = null;
|
||||
@@ -104,10 +142,9 @@ namespace KCM.LoadSaveOverrides
|
||||
clientPlayerData = kvp.Value;
|
||||
}
|
||||
else
|
||||
{ // Maybe ??
|
||||
{
|
||||
Main.helper.Log("Loading player data: " + kvp.Key);
|
||||
|
||||
|
||||
KCPlayer player;
|
||||
|
||||
if (!Main.kCPlayers.TryGetValue(kvp.Key, out player))
|
||||
@@ -120,39 +157,63 @@ namespace KCM.LoadSaveOverrides
|
||||
Player.inst = player.inst;
|
||||
Main.helper.Log($"Number of landmasses: {World.inst.NumLandMasses}");
|
||||
|
||||
//Reset was here before unpack
|
||||
kvp.Value.Unpack(player.inst);
|
||||
|
||||
Player.inst = oldPlayer;
|
||||
|
||||
|
||||
player.banner = player.inst.PlayerLandmassOwner.bannerIdx;
|
||||
player.kingdomName = TownNameUI.inst.townName;
|
||||
}
|
||||
}
|
||||
|
||||
clientPlayerData.Unpack(Player.inst); // Unpack the current client player last so that loading of villagers works correctly.
|
||||
clientPlayerData.Unpack(Player.inst);
|
||||
|
||||
Main.helper.Log("unpacked player data");
|
||||
Main.helper.Log("Setting banner and name");
|
||||
|
||||
var client = Main.kCPlayers[SteamUser.GetSteamID().ToString()];
|
||||
|
||||
|
||||
client.banner = Player.inst.PlayerLandmassOwner.bannerIdx;
|
||||
client.kingdomName = TownNameUI.inst.townName;
|
||||
|
||||
Main.helper.Log("Finished unpacking player data");
|
||||
|
||||
/*
|
||||
* Not even going to bother fixing AI brains save data yet, not in short-term roadmap
|
||||
*/
|
||||
|
||||
/*bool flag2 = this.AIBrainsSaveData != null;
|
||||
if (flag2)
|
||||
Main.helper.Log("Unpacking AI brains");
|
||||
bool flag10 = this.AIBrainsSaveData != null;
|
||||
if (flag10)
|
||||
{
|
||||
this.AIBrainsSaveData.UnpackPrePlayer(AIBrainsContainer.inst);
|
||||
}*/
|
||||
try
|
||||
{
|
||||
this.AIBrainsSaveData.Unpack(AIBrainsContainer.inst);
|
||||
Main.helper.Log("AI brains unpacked successfully");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Main.helper.Log("Error unpacking AI brains: " + e.Message);
|
||||
Main.helper.Log("Attempting to reinitialize AI systems");
|
||||
try
|
||||
{
|
||||
AIBrainsContainer.inst.ClearAIs();
|
||||
Main.helper.Log("AI systems reinitialized");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("Failed to reinitialize AI systems: " + ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Main.helper.Log("No AI brains save data found, initializing fresh AI");
|
||||
try
|
||||
{
|
||||
Main.helper.Log("Fresh AI initialization completed");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Main.helper.Log("Failed fresh AI initialization: " + e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
Main.helper.Log("Unpacking free resource manager");
|
||||
this.FreeResourceManagerSaveData.Unpack(FreeResourceManager.inst);
|
||||
@@ -207,7 +268,6 @@ namespace KCM.LoadSaveOverrides
|
||||
this.OrdersManagerSaveData.Unpack(OrdersManager.inst);
|
||||
}
|
||||
Main.helper.Log("Unpacking AI brains");
|
||||
bool flag10 = this.AIBrainsSaveData != null;
|
||||
if (flag10)
|
||||
{
|
||||
this.AIBrainsSaveData.Unpack(AIBrainsContainer.inst);
|
||||
@@ -220,6 +280,19 @@ namespace KCM.LoadSaveOverrides
|
||||
}
|
||||
Main.helper.Log("Unpacking done");
|
||||
|
||||
try
|
||||
{
|
||||
Main.helper.Log("Post-load: rebuilding path costs + villager grid");
|
||||
try { World.inst.SetupInitialPathCosts(); } catch (Exception e) { Main.helper.Log(e.ToString()); }
|
||||
try { World.inst.RebuildVillagerGrid(); } catch (Exception e) { Main.helper.Log(e.ToString()); }
|
||||
try { Player.inst.irrigation.UpdateIrrigation(); } catch (Exception e) { Main.helper.Log(e.ToString()); }
|
||||
try { Player.inst.CalcMaxResources(null, -1); } catch (Exception e) { Main.helper.Log(e.ToString()); }
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Main.helper.Log("Post-load rebuild failed");
|
||||
Main.helper.Log(e.ToString());
|
||||
}
|
||||
|
||||
World.inst.UpscaleFeatures();
|
||||
Player.inst.RefreshVisibility(true);
|
||||
@@ -228,28 +301,32 @@ namespace KCM.LoadSaveOverrides
|
||||
Player.inst.Buildings.data[i].UpdateMaterialSelection();
|
||||
}
|
||||
|
||||
// Player.inst.loadTickDelay = 1;
|
||||
Type playerType = typeof(Player);
|
||||
FieldInfo loadTickDelayField = playerType.GetField("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
if (loadTickDelayField != null)
|
||||
{
|
||||
loadTickDelayField.SetValue(Player.inst, 1);
|
||||
loadTickDelayField.SetValue(Player.inst, 3);
|
||||
}
|
||||
|
||||
// UnitSystem.inst.loadTickDelay = 1;
|
||||
Type unitSystemType = typeof(UnitSystem);
|
||||
loadTickDelayField = unitSystemType.GetField("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
if (loadTickDelayField != null)
|
||||
{
|
||||
loadTickDelayField.SetValue(UnitSystem.inst, 1);
|
||||
loadTickDelayField.SetValue(UnitSystem.inst, 3);
|
||||
}
|
||||
|
||||
// JobSystem.inst.loadTickDelay = 1;
|
||||
Type jobSystemType = typeof(JobSystem);
|
||||
loadTickDelayField = jobSystemType.GetField("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
if (loadTickDelayField != null)
|
||||
{
|
||||
loadTickDelayField.SetValue(JobSystem.inst, 1);
|
||||
loadTickDelayField.SetValue(JobSystem.inst, 3);
|
||||
}
|
||||
|
||||
Type villagerSystemType = typeof(VillagerSystem);
|
||||
loadTickDelayField = villagerSystemType.GetField("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
if (loadTickDelayField != null)
|
||||
{
|
||||
loadTickDelayField.SetValue(VillagerSystem.inst, 3);
|
||||
}
|
||||
|
||||
Main.helper.Log($"Setting kingdom name to: {kingdomNames[Main.PlayerSteamID]}");
|
||||
@@ -258,4 +335,4 @@ namespace KCM.LoadSaveOverrides
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,91 +0,0 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
|
||||
namespace KCM.Packets.Game.GameBuilding
|
||||
{
|
||||
public class BuildingRemovePacket : Packet
|
||||
{
|
||||
public override ushort packetId => (ushort)KCM.Enums.Packets.BuildingRemove;
|
||||
|
||||
// Flag to prevent infinite loop when removing buildings from packet
|
||||
public static bool isProcessingPacket = false;
|
||||
|
||||
public Guid guid { get; set; }
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
if (clientId == KCClient.client.Id) return;
|
||||
|
||||
Main.helper.Log($"Received building remove packet for guid {guid} from {player.name}");
|
||||
|
||||
// Try to find the building in the player who owns it
|
||||
Building building = player.inst.GetBuilding(guid);
|
||||
|
||||
if (building == null)
|
||||
{
|
||||
// Try to find it in any player's buildings
|
||||
foreach (var kcp in Main.kCPlayers.Values)
|
||||
{
|
||||
building = kcp.inst.GetBuilding(guid);
|
||||
if (building != null) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (building == null)
|
||||
{
|
||||
Main.helper.Log($"Building with guid {guid} not found on client, may already be removed.");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
Main.helper.Log($"Removing building {building.UniqueName} at {building.transform.position}");
|
||||
|
||||
// Set flag to prevent sending packet back
|
||||
isProcessingPacket = true;
|
||||
|
||||
// Set Player.inst to the correct player for this building
|
||||
// This ensures the removal modifies the correct player's job lists
|
||||
Player originalPlayer = Player.inst;
|
||||
Player correctPlayer = Main.GetPlayerByBuilding(building);
|
||||
if (correctPlayer != null)
|
||||
{
|
||||
Player.inst = correctPlayer;
|
||||
}
|
||||
|
||||
// Use reflection to call the Remove method from the game assembly
|
||||
MethodInfo removeMethod = typeof(Building).GetMethod("Remove", BindingFlags.Public | BindingFlags.Instance);
|
||||
if (removeMethod != null)
|
||||
{
|
||||
removeMethod.Invoke(building, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: destroy the building GameObject directly
|
||||
Main.helper.Log("Remove method not found, using Destroy fallback");
|
||||
building.destroyedWhileInPlay = true;
|
||||
UnityEngine.Object.Destroy(building.gameObject);
|
||||
}
|
||||
|
||||
// Restore original Player.inst
|
||||
Player.inst = originalPlayer;
|
||||
isProcessingPacket = false;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
isProcessingPacket = false;
|
||||
Main.helper.Log($"Error removing building: {e.Message}");
|
||||
Main.helper.Log(e.StackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
// Forward the remove packet to all other clients
|
||||
SendToAll(clientId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace KCM.Packets.Game
|
||||
{
|
||||
@@ -11,18 +12,76 @@ namespace KCM.Packets.Game
|
||||
public override ushort packetId => (int)Enums.Packets.SetSpeed;
|
||||
|
||||
public int speed { get; set; }
|
||||
public bool isPaused { get; set; }
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
if (clientId == KCClient.client.Id) // Prevent speed softlock
|
||||
return;
|
||||
|
||||
SpeedControlUI.inst.SetSpeed(speed);
|
||||
try
|
||||
{
|
||||
// Apply speed setting
|
||||
SpeedControlUI.inst.SetSpeed(speed);
|
||||
|
||||
// Handle pause/unpause state
|
||||
if (isPaused && Time.timeScale > 0)
|
||||
{
|
||||
// Game should be paused
|
||||
Time.timeScale = 0f;
|
||||
Main.helper.Log("Game paused via network sync");
|
||||
}
|
||||
else if (!isPaused && Time.timeScale == 0)
|
||||
{
|
||||
// Game should be unpaused - restore speed
|
||||
Time.timeScale = 1f;
|
||||
SpeedControlUI.inst.SetSpeed(speed);
|
||||
Main.helper.Log("Game unpaused via network sync");
|
||||
}
|
||||
|
||||
// Force AI system update when speed changes
|
||||
if (speed > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
// Force villager system refresh to ensure they continue working
|
||||
if (VillagerSystem.inst != null)
|
||||
{
|
||||
// Use reflection to call any refresh methods on VillagerSystem
|
||||
var villagerSystemType = typeof(VillagerSystem);
|
||||
var refreshMethods = villagerSystemType.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
|
||||
.Where(m => m.Name.Contains("Refresh") || m.Name.Contains("Update") || m.Name.Contains("Restart"));
|
||||
|
||||
foreach (var method in refreshMethods)
|
||||
{
|
||||
if (method.GetParameters().Length == 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
method.Invoke(VillagerSystem.inst, null);
|
||||
Main.helper.Log($"Called VillagerSystem.{method.Name} for speed change");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
Main.helper.Log($"AI systems refreshed for speed change to {speed}");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Main.helper.Log("Error refreshing AI on speed change: " + e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Main.helper.Log("Error handling SetSpeed packet: " + e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
//throw new NotImplementedException();
|
||||
// Server doesn't need to handle this packet
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ namespace KCM.Packets.Handlers
|
||||
if (!KCServer.IsRunning)
|
||||
{
|
||||
Main.kCPlayers.Clear();
|
||||
Main.clientSteamIds.Clear();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -84,7 +84,9 @@ namespace KCM.Packets.Handlers
|
||||
{
|
||||
|
||||
IPacket p = (IPacket)Activator.CreateInstance(packet);
|
||||
var properties = packet.GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(prop => prop.Name != "packetId").ToArray();
|
||||
var properties = packet.GetProperties(BindingFlags.Instance | BindingFlags.Public)
|
||||
.Where(prop => prop.Name != "packetId" && prop.Name != "sendMode")
|
||||
.ToArray();
|
||||
Array.Sort(properties, (x, y) => String.Compare(x.Name, y.Name));
|
||||
ushort id = (ushort)p.GetType().GetProperty("packetId").GetValue(p, null);
|
||||
|
||||
@@ -137,7 +139,9 @@ namespace KCM.Packets.Handlers
|
||||
{
|
||||
packet.HandlePacketServer();
|
||||
|
||||
((Packet)packet).SendToAll();
|
||||
bool shouldRelay = packet.GetType().GetCustomAttributes(typeof(NoServerRelayAttribute), inherit: true).Length == 0;
|
||||
if (shouldRelay)
|
||||
((Packet)packet).SendToAll();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -222,14 +226,20 @@ namespace KCM.Packets.Handlers
|
||||
try
|
||||
{
|
||||
var packetRef = Packets[packet.packetId];
|
||||
Message message = Message.Create(MessageSendMode.Reliable, packet.packetId);
|
||||
|
||||
MessageSendMode sendMode = MessageSendMode.Reliable;
|
||||
Packet basePacket = packet as Packet;
|
||||
if (basePacket != null)
|
||||
sendMode = basePacket.sendMode;
|
||||
|
||||
Message message = Message.Create(sendMode, packet.packetId);
|
||||
|
||||
foreach (var prop in packetRef.properties)
|
||||
{
|
||||
if (prop.PropertyType.IsEnum)
|
||||
{
|
||||
currentPropName = prop.Name;
|
||||
message.AddInt((int)prop.GetValue(packet, null));
|
||||
message.AddInt(Convert.ToInt32(prop.GetValue(packet, null)));
|
||||
}
|
||||
else if (prop.PropertyType == typeof(ushort))
|
||||
{
|
||||
@@ -461,9 +471,7 @@ namespace KCM.Packets.Handlers
|
||||
if (prop.PropertyType.IsEnum)
|
||||
{
|
||||
int enumValue = message.GetInt();
|
||||
string enumName = Enum.GetName(prop.PropertyType, enumValue);
|
||||
|
||||
prop.SetValue(p, Enum.Parse(prop.PropertyType, enumName));
|
||||
prop.SetValue(p, Enum.ToObject(prop.PropertyType, enumValue));
|
||||
}
|
||||
else if (prop.PropertyType == typeof(ushort))
|
||||
{
|
||||
|
||||
@@ -35,13 +35,17 @@ namespace KCM.Packets.Lobby
|
||||
|
||||
Main.helper.Log("PlayerList: " + playersName[i] + " " + playersId[i] + " " + steamIds[i]);
|
||||
|
||||
Main.kCPlayers.Add(steamIds[i], new KCPlayer(playersName[i], playersId[i], steamIds[i])
|
||||
KCPlayer player;
|
||||
if (!Main.kCPlayers.TryGetValue(steamIds[i], out player) || player == null)
|
||||
{
|
||||
name = playersName[i],
|
||||
ready = playersReady[i],
|
||||
banner = playersBanner[i],
|
||||
kingdomName = playersKingdomName[i]
|
||||
});
|
||||
player = new KCPlayer(playersName[i], playersId[i], steamIds[i]);
|
||||
Main.kCPlayers[steamIds[i]] = player;
|
||||
}
|
||||
|
||||
player.name = playersName[i];
|
||||
player.ready = playersReady[i];
|
||||
player.banner = playersBanner[i];
|
||||
player.kingdomName = playersKingdomName[i];
|
||||
|
||||
|
||||
if (Main.clientSteamIds.ContainsKey(playersId[i]))
|
||||
@@ -49,7 +53,8 @@ namespace KCM.Packets.Lobby
|
||||
else
|
||||
Main.clientSteamIds.Add(playersId[i], steamIds[i]);
|
||||
|
||||
Main.kCPlayers[steamIds[i]].inst.PlayerLandmassOwner.SetBannerIdx(playersBanner[i]);
|
||||
if (player.inst != null && player.inst.PlayerLandmassOwner != null)
|
||||
player.inst.PlayerLandmassOwner.SetBannerIdx(playersBanner[i]);
|
||||
|
||||
LobbyHandler.AddPlayerEntry(playersId[i]);
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ namespace KCM.Packets.Lobby
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
if (player == null)
|
||||
return;
|
||||
IsReady = !player.ready;
|
||||
//SendToAll(KCClient.client.Id);
|
||||
|
||||
@@ -22,6 +24,8 @@ namespace KCM.Packets.Lobby
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
if (player == null)
|
||||
return;
|
||||
player.ready = IsReady;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Riptide.Demos.Steam.PlayerHosted;
|
||||
using KCM.StateManagement.Observers;
|
||||
using static KCM.Main;
|
||||
|
||||
namespace KCM.Packets.Lobby
|
||||
@@ -18,6 +18,14 @@ namespace KCM.Packets.Lobby
|
||||
public static bool loadingSave = false;
|
||||
public static int received = 0;
|
||||
|
||||
public static void ResetTransferState()
|
||||
{
|
||||
loadingSave = false;
|
||||
received = 0;
|
||||
saveData = new byte[1];
|
||||
chunksReceived = new bool[1];
|
||||
}
|
||||
|
||||
|
||||
public int chunkId { get; set; }
|
||||
public int chunkSize { get; set; }
|
||||
@@ -30,90 +38,123 @@ namespace KCM.Packets.Lobby
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
// Initialize on first chunk OR if arrays aren't properly sized yet
|
||||
// This handles out-of-order packet delivery
|
||||
if (!loadingSave || saveData.Length != saveSize || chunksReceived.Length != totalChunks)
|
||||
bool initialisingTransfer = !loadingSave ||
|
||||
saveData == null ||
|
||||
saveData.Length != saveSize ||
|
||||
chunksReceived == null ||
|
||||
chunksReceived.Length != totalChunks;
|
||||
|
||||
if (initialisingTransfer)
|
||||
{
|
||||
Main.helper.Log($"Save Transfer initializing. saveSize={saveSize}, totalChunks={totalChunks}");
|
||||
Main.helper.Log("Save Transfer started!");
|
||||
loadingSave = true;
|
||||
received = 0;
|
||||
|
||||
StateObserver.ClearAll();
|
||||
|
||||
saveData = new byte[saveSize];
|
||||
chunksReceived = new bool[totalChunks];
|
||||
received = 0;
|
||||
|
||||
ServerLobbyScript.LoadingSave.SetActive(true);
|
||||
if (ServerLobbyScript.LoadingSave != null)
|
||||
ServerLobbyScript.LoadingSave.SetActive(true);
|
||||
}
|
||||
|
||||
// Skip if we already received this chunk (duplicate packet)
|
||||
if (chunksReceived[chunkId])
|
||||
if (chunkId < 0 || chunkId >= totalChunks)
|
||||
{
|
||||
Main.helper.Log($"[SaveTransfer] Duplicate chunk {chunkId} received, skipping.");
|
||||
Main.helper.Log($"Invalid save chunk id: {chunkId} / {totalChunks}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (saveDataChunk == null)
|
||||
{
|
||||
Main.helper.Log($"Null save chunk data for chunk: {chunkId}");
|
||||
return;
|
||||
}
|
||||
|
||||
if (saveDataIndex < 0 || saveDataIndex + saveDataChunk.Length > saveData.Length)
|
||||
{
|
||||
Main.helper.Log($"Invalid save chunk write range: index={saveDataIndex} len={saveDataChunk.Length} size={saveData.Length}");
|
||||
return;
|
||||
}
|
||||
|
||||
Array.Copy(saveDataChunk, 0, saveData, saveDataIndex, saveDataChunk.Length);
|
||||
|
||||
chunksReceived[chunkId] = true;
|
||||
|
||||
received += chunkSize;
|
||||
|
||||
Main.helper.Log($"[SaveTransfer] Processed chunk {chunkId}/{totalChunks}. Received: {received} bytes of {saveSize}.");
|
||||
|
||||
// Update progress bar
|
||||
if (saveSize > 0)
|
||||
{
|
||||
float savePercent = (float)received / (float)saveSize;
|
||||
string receivedKB = ((float)received / 1000f).ToString("0.00");
|
||||
string totalKB = ((float)saveSize / 1000f).ToString("0.00");
|
||||
|
||||
float savePercent = saveSize > 0 ? (float)received / (float)saveSize : 0f;
|
||||
if (ServerLobbyScript.ProgressBar != null)
|
||||
ServerLobbyScript.ProgressBar.fillAmount = savePercent;
|
||||
if (ServerLobbyScript.ProgressBarText != null)
|
||||
ServerLobbyScript.ProgressBarText.text = (savePercent * 100).ToString("0.00") + "%";
|
||||
ServerLobbyScript.ProgressText.text = $"{receivedKB} KB / {totalKB} KB";
|
||||
}
|
||||
else
|
||||
if (ServerLobbyScript.ProgressText != null)
|
||||
ServerLobbyScript.ProgressText.text = $"{((float)(received / 1000)).ToString("0.00")} KB / {((float)(saveSize / 1000)).ToString("0.00")} KB";
|
||||
|
||||
|
||||
if (chunkId + 1 == totalChunks)
|
||||
{
|
||||
ServerLobbyScript.ProgressBar.fillAmount = 0f;
|
||||
ServerLobbyScript.ProgressBarText.text = "0.00%";
|
||||
ServerLobbyScript.ProgressText.text = "0.00 KB / 0.00 KB";
|
||||
Main.helper.Log($"Received last save transfer packet.");
|
||||
|
||||
Main.helper.Log(WhichIsNotComplete());
|
||||
}
|
||||
|
||||
// Check if all chunks have been received
|
||||
if (IsTransferComplete())
|
||||
{
|
||||
// Handle completed transfer here
|
||||
Main.helper.Log("Save Transfer complete!");
|
||||
|
||||
// Reset the loading state before processing
|
||||
loadingSave = false;
|
||||
|
||||
LoadSaveLoadHook.saveBytes = saveData;
|
||||
LoadSaveLoadHook.memoryStreamHook = true;
|
||||
KCM.LoadSaveLoadHook.saveBytes = saveData;
|
||||
KCM.LoadSaveLoadHook.memoryStreamHook = true;
|
||||
|
||||
LoadSave.Load();
|
||||
|
||||
GameState.inst.SetNewMode(GameState.inst.playingMode);
|
||||
LobbyManager.loadingSave = false;
|
||||
|
||||
try
|
||||
{
|
||||
Main.SetMultiplayerSaveLoadInProgress(true);
|
||||
KCM.LoadSaveLoadHook.saveContainer.Unpack(null);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Main.SetMultiplayerSaveLoadInProgress(false);
|
||||
}
|
||||
Broadcast.OnLoadedEvent.Broadcast(new OnLoadedEvent());
|
||||
|
||||
ServerLobbyScript.LoadingSave.SetActive(false);
|
||||
try
|
||||
{
|
||||
new KCM.Packets.Network.ResyncRequestPacket { reason = "post-load" }.Send();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
// Reset static state for next transfer
|
||||
ResetTransferState();
|
||||
if (ServerLobbyScript.LoadingSave != null)
|
||||
ServerLobbyScript.LoadingSave.SetActive(false);
|
||||
|
||||
loadingSave = false;
|
||||
received = 0;
|
||||
saveData = new byte[1];
|
||||
chunksReceived = new bool[1];
|
||||
}
|
||||
}
|
||||
|
||||
public static void ResetTransferState()
|
||||
{
|
||||
saveData = new byte[1];
|
||||
chunksReceived = new bool[1];
|
||||
loadingSave = false;
|
||||
received = 0;
|
||||
}
|
||||
|
||||
public static bool IsTransferComplete()
|
||||
{
|
||||
return chunksReceived.All(x => x == true);
|
||||
}
|
||||
|
||||
public static string WhichIsNotComplete()
|
||||
{
|
||||
string notComplete = "";
|
||||
for (int i = 0; i < chunksReceived.Length; i++)
|
||||
{
|
||||
if (!chunksReceived[i])
|
||||
{
|
||||
notComplete += i + ", ";
|
||||
}
|
||||
}
|
||||
return notComplete;
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
}
|
||||
|
||||
@@ -18,26 +18,38 @@ namespace KCM.Packets.Lobby
|
||||
{
|
||||
Main.helper.Log(GameState.inst.mainMenuMode.ToString());
|
||||
|
||||
// Hide server lobby
|
||||
Main.TransitionTo((MenuState)200);
|
||||
|
||||
// This is run when user clicks "accept" on choose your map screeen
|
||||
|
||||
try
|
||||
{
|
||||
SpeedControlUI.inst.SetSpeed(0);
|
||||
|
||||
try
|
||||
if (!LobbyManager.loadingSave)
|
||||
{
|
||||
typeof(MainMenuMode).GetMethod("StartGame", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(GameState.inst.mainMenuMode, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log(ex.Message.ToString());
|
||||
Main.helper.Log(ex.ToString());
|
||||
}
|
||||
SpeedControlUI.inst.SetSpeed(0);
|
||||
|
||||
SpeedControlUI.inst.SetSpeed(0);
|
||||
try
|
||||
{
|
||||
typeof(MainMenuMode).GetMethod("StartGame", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(GameState.inst.mainMenuMode, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log(ex.Message.ToString());
|
||||
Main.helper.Log(ex.ToString());
|
||||
}
|
||||
|
||||
SpeedControlUI.inst.SetSpeed(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
LobbyManager.loadingSave = false;
|
||||
GameState.inst.SetNewMode(GameState.inst.playingMode);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Handle exception here
|
||||
Main.helper.Log(ex.Message.ToString());
|
||||
Main.helper.Log(ex.ToString());
|
||||
}
|
||||
@@ -45,18 +57,47 @@ namespace KCM.Packets.Lobby
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
if (!LobbyManager.loadingSave)
|
||||
{
|
||||
Start();
|
||||
}
|
||||
else
|
||||
{
|
||||
ServerLobbyScript.LoadingSave.SetActive(true);
|
||||
}
|
||||
Start();
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
//Start();
|
||||
|
||||
|
||||
/*AIBrainsContainer.PreStartAIConfig aiConfig = new AIBrainsContainer.PreStartAIConfig();
|
||||
int count = 0;
|
||||
for (int i = 0; i < RivalKingdomSettingsUI.inst.rivalItems.Length; i++)
|
||||
{
|
||||
RivalItemUI r = RivalKingdomSettingsUI.inst.rivalItems[i];
|
||||
bool flag = r.Enabled && !r.Locked;
|
||||
if (flag)
|
||||
{
|
||||
count++;
|
||||
}
|
||||
}
|
||||
int idx = 0;
|
||||
aiConfig.startData = new AIBrainsContainer.PreStartAIConfig.AIStartData[count];
|
||||
for (int j = 0; j < RivalKingdomSettingsUI.inst.rivalItems.Length; j++)
|
||||
{
|
||||
RivalItemUI item = RivalKingdomSettingsUI.inst.rivalItems[j];
|
||||
bool flag2 = item.Enabled && !item.Locked;
|
||||
if (flag2)
|
||||
{
|
||||
aiConfig.startData[idx] = new AIBrainsContainer.PreStartAIConfig.AIStartData();
|
||||
aiConfig.startData[idx].landmass = item.flag.landmass;
|
||||
aiConfig.startData[idx].bioCode = item.bannerIdx;
|
||||
aiConfig.startData[idx].personalityKey = PersonalityCollection.aiPersonalityKeys[0];
|
||||
aiConfig.startData[idx].skillLevel = item.GetSkillLevel();
|
||||
idx++;
|
||||
}
|
||||
}
|
||||
AIBrainsContainer.inst.aiStartInfo = aiConfig;
|
||||
bool isControllerActive = GamepadControl.inst.isControllerActive;
|
||||
if (isControllerActive)
|
||||
{
|
||||
ConsoleCursorMenu.inst.PrepForGamepad();
|
||||
}*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,14 @@ namespace KCM.Packets.Lobby
|
||||
World.inst.Generate(Seed);
|
||||
Vector3 center = World.inst.GetCellData(World.inst.GridWidth / 2, World.inst.GridHeight / 2).Center;
|
||||
Cam.inst.SetTrackingPos(center);
|
||||
|
||||
try
|
||||
{
|
||||
new KCM.Packets.Network.ResyncRequestPacket { reason = "post-world-seed" }.Send();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
|
||||
@@ -51,6 +51,20 @@ namespace KCM.Packets.Network
|
||||
{
|
||||
Main.helper.Log("Server Player Connected: " + Name + " Id: " + clientId + " SteamID: " + SteamId);
|
||||
|
||||
KCPlayer player;
|
||||
if (Main.kCPlayers.TryGetValue(SteamId, out player))
|
||||
{
|
||||
player.id = clientId;
|
||||
player.name = Name;
|
||||
player.steamId = SteamId;
|
||||
}
|
||||
else
|
||||
{
|
||||
Main.kCPlayers[SteamId] = new KCPlayer(Name, clientId, SteamId);
|
||||
}
|
||||
|
||||
Main.clientSteamIds[clientId] = SteamId;
|
||||
|
||||
List<KCPlayer> list = Main.kCPlayers.Select(x => x.Value).OrderBy(x => x.id).ToList();
|
||||
|
||||
if (list.Count > 0)
|
||||
@@ -78,33 +92,7 @@ namespace KCM.Packets.Network
|
||||
return;
|
||||
|
||||
byte[] bytes = LoadSaveLoadAtPathHook.saveData;
|
||||
int chunkSize = 900; // 900 bytes per chunk to fit within packet size limit
|
||||
|
||||
List<byte[]> chunks = SplitByteArrayIntoChunks(bytes, chunkSize);
|
||||
Main.helper.Log("Save Transfer started!");
|
||||
|
||||
int sent = 0;
|
||||
int packetsSent = 0;
|
||||
|
||||
for (int i = 0; i < chunks.Count; i++)
|
||||
{
|
||||
var chunk = chunks[i];
|
||||
|
||||
|
||||
new SaveTransferPacket()
|
||||
{
|
||||
saveSize = bytes.Length,
|
||||
saveDataChunk = chunk,
|
||||
chunkId = i,
|
||||
chunkSize = chunk.Length,
|
||||
saveDataIndex = sent,
|
||||
totalChunks = chunks.Count
|
||||
}.SendReliable(clientId);
|
||||
packetsSent++;
|
||||
sent += chunk.Length;
|
||||
}
|
||||
|
||||
Main.helper.Log($"Sent {packetsSent} save data chunks to client");
|
||||
KCServer.EnqueueSaveTransfer(clientId, bytes);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
35
Packets/Network/ResyncRequestPacket.cs
Normal file
35
Packets/Network/ResyncRequestPacket.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using KCM.Attributes;
|
||||
using KCM.StateManagement.Sync;
|
||||
using System;
|
||||
|
||||
namespace KCM.Packets.Network
|
||||
{
|
||||
[NoServerRelay]
|
||||
public class ResyncRequestPacket : Packet
|
||||
{
|
||||
public override ushort packetId => (ushort)Enums.Packets.ResyncRequest;
|
||||
|
||||
public string reason { get; set; }
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!KCServer.IsRunning)
|
||||
return;
|
||||
|
||||
SyncManager.SendResyncToClient(clientId, reason);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("Error handling ResyncRequestPacket on server");
|
||||
Main.helper.Log(ex.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,8 @@ namespace KCM.Packets.Network
|
||||
|
||||
Main.helper.Log("Sending client connected. Client ID is: " + clientId);
|
||||
|
||||
Main.kCPlayers.Add(Main.PlayerSteamID, new KCPlayer(KCClient.inst.Name, clientId, Main.PlayerSteamID));
|
||||
Main.kCPlayers[Main.PlayerSteamID] = new KCPlayer(KCClient.inst.Name, clientId, Main.PlayerSteamID);
|
||||
Main.clientSteamIds[clientId] = Main.PlayerSteamID;
|
||||
|
||||
Player.inst.PlayerLandmassOwner.teamId = clientId * 10 + 2;
|
||||
|
||||
|
||||
@@ -11,22 +11,21 @@ namespace KCM.Packets
|
||||
{
|
||||
public abstract ushort packetId { get; }
|
||||
public ushort clientId { get; set; }
|
||||
public virtual Riptide.MessageSendMode sendMode => Riptide.MessageSendMode.Reliable;
|
||||
|
||||
public KCPlayer player
|
||||
{
|
||||
get
|
||||
{
|
||||
KCPlayer p = null;
|
||||
|
||||
if (!Main.clientSteamIds.ContainsKey(clientId))
|
||||
string steamId;
|
||||
if (!Main.clientSteamIds.TryGetValue(clientId, out steamId) || string.IsNullOrEmpty(steamId))
|
||||
return null;
|
||||
if (Main.kCPlayers.TryGetValue(Main.GetPlayerByClientID(clientId).steamId, out p))
|
||||
return p;
|
||||
else
|
||||
{
|
||||
Main.helper.Log($"Error getting player from packet {packetId} {this.GetType().Name} from {clientId}");
|
||||
}
|
||||
|
||||
KCPlayer player;
|
||||
if (Main.kCPlayers.TryGetValue(steamId, out player))
|
||||
return player;
|
||||
|
||||
Main.helper.Log($"Error getting player from packet {packetId} {GetType().Name} from {clientId}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -104,37 +103,6 @@ namespace KCM.Packets
|
||||
}
|
||||
}
|
||||
|
||||
public void SendReliable(ushort toClient)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (KCServer.IsRunning && toClient != 0)
|
||||
{
|
||||
KCServer.server.Send(PacketHandler.SerialisePacket(this), toClient, true);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log($"Error sending reliable packet {packetId} {this.GetType().Name} from {clientId}");
|
||||
|
||||
Main.helper.Log("----------------------- Main exception -----------------------");
|
||||
Main.helper.Log(ex.ToString());
|
||||
Main.helper.Log("----------------------- Main message -----------------------");
|
||||
Main.helper.Log(ex.Message);
|
||||
Main.helper.Log("----------------------- Main stacktrace -----------------------");
|
||||
Main.helper.Log(ex.StackTrace);
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
Main.helper.Log("----------------------- Inner exception -----------------------");
|
||||
Main.helper.Log(ex.InnerException.ToString());
|
||||
Main.helper.Log("----------------------- Inner message -----------------------");
|
||||
Main.helper.Log(ex.InnerException.Message);
|
||||
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
|
||||
Main.helper.Log(ex.InnerException.StackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void HandlePacketServer();
|
||||
public abstract void HandlePacketClient();
|
||||
}
|
||||
|
||||
33
Packets/State/BuildingSnapshotPacket.cs
Normal file
33
Packets/State/BuildingSnapshotPacket.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using KCM.StateManagement.Sync;
|
||||
using System;
|
||||
|
||||
namespace KCM.Packets.State
|
||||
{
|
||||
public class BuildingSnapshotPacket : Packet
|
||||
{
|
||||
public override ushort packetId => (ushort)Enums.Packets.BuildingSnapshot;
|
||||
|
||||
public byte[] payload { get; set; }
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (payload == null || payload.Length == 0)
|
||||
return;
|
||||
|
||||
SyncManager.ApplyBuildingSnapshot(payload);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("Error applying BuildingSnapshotPacket");
|
||||
Main.helper.Log(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace KCM.Packets.State
|
||||
public class BuildingStatePacket : Packet
|
||||
{
|
||||
public override ushort packetId => (ushort)Enums.Packets.BuildingStatePacket;
|
||||
public override Riptide.MessageSendMode sendMode => Riptide.MessageSendMode.Unreliable;
|
||||
|
||||
public string customName { get; set; }
|
||||
public Guid guid { get; set; }
|
||||
|
||||
37
Packets/State/ResourceSnapshotPacket.cs
Normal file
37
Packets/State/ResourceSnapshotPacket.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using KCM.StateManagement.Sync;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace KCM.Packets.State
|
||||
{
|
||||
public class ResourceSnapshotPacket : Packet
|
||||
{
|
||||
public override ushort packetId => (ushort)Enums.Packets.ResourceSnapshot;
|
||||
|
||||
public override Riptide.MessageSendMode sendMode => Riptide.MessageSendMode.Unreliable;
|
||||
|
||||
public List<int> resourceTypes { get; set; }
|
||||
public List<int> amounts { get; set; }
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (KCClient.client.IsConnected && KCClient.client.Id == clientId)
|
||||
return;
|
||||
|
||||
SyncManager.ApplyResourceSnapshot(resourceTypes, amounts);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("Error applying ResourceSnapshotPacket");
|
||||
Main.helper.Log(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,11 +24,12 @@ namespace KCM
|
||||
{
|
||||
try
|
||||
{
|
||||
//Main.helper = _helper;
|
||||
Main.helper = _helper;
|
||||
Main.EnsureNetworking();
|
||||
|
||||
assetBundle = KCModHelper.LoadAssetBundle(_helper.modPath, "serverbrowserpkg");
|
||||
|
||||
Main.helper.Log(String.Join(", ", assetBundle.GetAllAssetNames()));
|
||||
Main.Log(String.Join(", ", assetBundle.GetAllAssetNames()));
|
||||
|
||||
serverBrowserPrefab = assetBundle.LoadAsset("assets/workspace/serverbrowser.prefab") as GameObject;
|
||||
serverEntryItemPrefab = assetBundle.LoadAsset("assets/workspace/serverentryitem.prefab") as GameObject;
|
||||
@@ -41,13 +42,13 @@ namespace KCM
|
||||
|
||||
modalUIPrefab = assetBundle.LoadAsset("assets/workspace/modalui.prefab") as GameObject;
|
||||
|
||||
Main.helper.Log("Loaded assets");
|
||||
Main.Log("Loaded assets");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log(ex.ToString());
|
||||
Main.helper.Log(ex.Message);
|
||||
Main.helper.Log(ex.StackTrace);
|
||||
Main.Log(ex);
|
||||
Main.Log(ex.Message);
|
||||
Main.Log(ex.StackTrace);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
91
README.md
91
README.md
@@ -1,38 +1,75 @@
|
||||
# Kingdoms and Castles Multiplayer Mod Fixes
|
||||
# KCM (Kingdoms and Castles Multiplayer)
|
||||
|
||||
This document summarizes the fixes and improvements implemented to enhance the stability and functionality of the multiplayer mod for Kingdoms and Castles.
|
||||
Ez a repó egy *Kingdoms and Castles* multiplayer mod forrása. A mod Steam lobby + Riptide alapú hálózattal próbálja a világot és a játékosok akcióit több kliens között szinkronban tartani.
|
||||
|
||||
## Implemented Fixes:
|
||||
Ha a `output.txt` logban `Compilation failed` szerepel, akkor a mod **nem töltődött be**, és semmi nem fog szinkronizálódni (ilyenkor tipikusan C# szintaxis / runtime-kompatibilitási hiba van a forrásban).
|
||||
|
||||
### 1. Improved Lobby Stability
|
||||
- **Issue:** Previously, joining a multiplayer lobby could lead to an immediate crash (NullReferenceException in `PlayerEntryScript.cs`).
|
||||
- **Fix:** Corrected the initialization order of UI components in `PlayerEntryScript.cs` to prevent NullReferenceExceptions, ensuring stable lobby entry.
|
||||
## Mit szinkronizál a mod? (jelenlegi állapot)
|
||||
|
||||
### 2. Enhanced Session Cleanup
|
||||
- **Issue:** Users previously had to restart the entire game after leaving a multiplayer session to join or host a new one. This was due to residual game state and an aggressive cleanup that inadvertently shut down Steamworks.
|
||||
- **Fix:** Implemented a comprehensive `CleanupMultiplayerSession()` routine in `Main.cs`. This routine now properly resets static mod data (player lists, client/server states), and, crucially, no longer destroys the core `KCMSteamManager` (Steamworks API manager). This allows for seamless transitions between multiplayer sessions without game restarts.
|
||||
**Lobby / kapcsolat**
|
||||
- Játékos csatlakozás/leválás, player lista, ready állapot.
|
||||
- Szerver beállítások (név, max players, seed, world opciók).
|
||||
- Chat és rendszerüzenetek.
|
||||
|
||||
### 3. Optimized Building Synchronization Performance
|
||||
- **Issue:** Rapid changes in building state (e.g., during construction) could generate excessive network traffic, potentially contributing to "poor connection" issues.
|
||||
- **Fix:** Implemented a throttling mechanism in `BuildingStateManager.cs`. Building state updates are now limited to 10 times per second per building, significantly reducing network spam while maintaining visual fluidity.
|
||||
**Világ indítás**
|
||||
- World seed szétküldése és world generálás a klienseken.
|
||||
- (Beállítástól függően) keep elhelyezés csomagból.
|
||||
|
||||
### 4. Resolved Villager Freezing
|
||||
- **Issue:** Villagers would sometimes freeze during gameplay, despite other game elements functioning correctly. This was caused by the game attempting to synchronize the state of already destroyed building components, leading to a cascade of errors.
|
||||
- **Fix:** Added a robust null check in `BuildingStateManager.cs`. If an observed building has been destroyed, its associated observer is now properly de-registered (by destroying its GameObject), preventing further errors and ensuring continuous game logic for villagers and other entities. This also handles cases where buildings are replaced (e.g., construction completed).
|
||||
**Gameplay alap**
|
||||
- Épület lerakás események (alap meta: `uniqueName`, `guid`, pozíció/rotáció).
|
||||
- Épület állapot frissítések “snapshot” jelleggel (`BuildingStatePacket`): built/placed, constructionProgress, life, stb.
|
||||
- Néhány globális esemény: idősebesség változtatás, időjárás váltás, fa kivágás (repo verziótól függően).
|
||||
- Host oldalon periodikus *resource snapshot* korrigálás (ha drift/desync van, visszahúzza a klienst).
|
||||
|
||||
### 5. Fixed Map Desynchronization
|
||||
- **Issue:** When starting a new multiplayer game, clients often generated a different map than the host, even if the seed was specified. This was due to the host not sending the definitive world seed at the critical moment.
|
||||
- **Fix:** Modified `ServerLobbyScript.cs` to ensure that when the host clicks "Start Game", the current world seed (either from UI input or newly generated) is explicitly sent to all clients via a `WorldSeed` packet *before* the game starts. This guarantees all players generate the exact same map.
|
||||
**Mentés betöltés (host → kliens)**
|
||||
- Host oldalon a mentés byte-ok chunkolva kerülnek kiküldésre (`SaveTransferPacket`).
|
||||
- Kliens oldalon érkezés után `LoadSave.Load()` + `MultiplayerSaveContainer.Unpack()` fut.
|
||||
- Ha a kiválasztott mentés nem multiplayer container (vanilla mentés), a host fallback-ként átadja a normál betöltést.
|
||||
|
||||
### 6. Reliable Save Game Transfer
|
||||
- **Issue:** Loading a saved multiplayer game would often fail for clients, resulting in an incomplete save file and desynchronized gameplay. This occurred because save file chunks were sent unreliably over the network.
|
||||
- **Fix:** Changed the save game chunk transfer in `ClientConnected.cs` to use Riptide's `Reliable` message send mode. This ensures that all parts of the save file are guaranteed to arrive at the client, allowing for complete and successful save game loading.
|
||||
## Mi nincs (még) rendesen szinkronizálva? (gyakori desync okok)
|
||||
|
||||
### 7. Compilation Errors & Warnings Addressed
|
||||
- All reported compilation errors and warnings (including issues with `Packet.Send` overloads and `World.SeedFromText`) have been investigated and resolved, ensuring the mod compiles cleanly.
|
||||
Ezek okozzák a tipikus “farm termel, de nem látszik” / “resource nem frissül” / “animáció hiányzik” jelenségeket:
|
||||
- **Erőforrás-logika és szállítás**: raktárkészletek, haul/cart routing, villager “viszem/lerakom” animációk nincsenek teljes állapotban szinkronizálva.
|
||||
- **Villager/job részletek**: current task, target, carried resource, pathing cache, részfeladat-állapot.
|
||||
- **Field/Farm belső állapot**: growth stage, harvest queue, field regisztráció edge case-ek.
|
||||
- **UI / kliens oldali state**: beragadt menük, promptok (pl. “rakd le a kezdő épületet”), lokális UI state nem hálózati adat.
|
||||
- **AI brains / nem-player rendszerek**: részben vagy egyáltalán nincs “szerver az igazság” modell.
|
||||
|
||||
## Pending Task:
|
||||
## Mit érdemes még hozzáadni? (roadmap)
|
||||
|
||||
### Resource Synchronization
|
||||
- **Goal:** Implement synchronization for player resources (Gold, Wood, Stone, Food) to ensure all players see consistent resource counts.
|
||||
- **Status:** Awaiting confirmation from the user regarding the exact `FreeResourceType` enum names (`Wood`, `Stone`, `Food`) to proceed with implementation.
|
||||
Ha cél a stabil “load utáni sync” és kevesebb vizuális desync:
|
||||
- **Resource szinkron**: raktárak készlete, termelés/fogyasztás tick eredménye, szállítási queue események (event-based vagy periodikus snapshot).
|
||||
- **Villager szinkron**: villager state machine + carried resource + célpont; vagy determinisztikus szerver oldali szimuláció és kliens “replay”.
|
||||
- **Farm/Field szinkron**: field állapot (growth/ready/harvest), aratás események explicit hálózati üzenetként.
|
||||
- **Robusztus reconnect**: kilépés egy sessionből → másik lobby csatlakozás restart nélkül (minden statikus állapot, observer, transfer state, player cache teljes resetje).
|
||||
- **Debug eszközök**: desync detektor (hash/snapshot összehasonlítás), több log a load/sync pontokra.
|
||||
|
||||
## Telepítés
|
||||
|
||||
- Hostnak és **minden kliensnek ugyanaz a mod verzió** kell.
|
||||
- Workshop verzió frissítés felülírhatja a módosításokat. Ajánlott:
|
||||
- kimásolni a modot a játék `...\\KingdomsAndCastles_Data\\mods\\` mappájába egy külön névvel,
|
||||
- és a mod menüben kikapcsolni a Workshop verziót.
|
||||
- Változtatások után **teljes játék újraindítás** javasolt.
|
||||
|
||||
## Hibaelhárítás
|
||||
|
||||
**Log helye:** a mod mappájában gyakran van `output.txt`.
|
||||
|
||||
Nézd ezeket a kulcssorokat:
|
||||
- `Compilation failed` → a mod nem fordult le, nincs multiplayer.
|
||||
- `Save Transfer started/complete` → mentés átküldés/betöltés állapota.
|
||||
- `Error loading save` / `LoadError` → sérült/rossz típusú mentés, vagy verzió eltérés.
|
||||
|
||||
Bug reporthoz küldd el:
|
||||
- a hiba környéki 50–100 sort a `output.txt`-ből,
|
||||
- host/kliens szerep, játék verzió, mod verzió,
|
||||
- új világban vagy mentés betöltés után jelentkezik-e.
|
||||
|
||||
## Fejlesztés
|
||||
|
||||
Repo-szabályok és szerkezet: `AGENTS.md`.
|
||||
|
||||
### Gyors resync
|
||||
|
||||
A lobby chatben írd be: `/resync` – a kliens kér egy resync-et a hosttól (resource + building + villager “teleport” snapshot).
|
||||
|
||||
@@ -49,7 +49,7 @@ public class KCMSteamManager : MonoBehaviour {
|
||||
|
||||
[AOT.MonoPInvokeCallback(typeof(SteamAPIWarningMessageHook_t))]
|
||||
protected static void SteamAPIDebugTextHook(int nSeverity, System.Text.StringBuilder pchDebugText) {
|
||||
Main.helper.Log(pchDebugText.ToString());
|
||||
Main.Log(pchDebugText.ToString());
|
||||
}
|
||||
|
||||
#if UNITY_2019_3_OR_NEWER
|
||||
@@ -64,7 +64,7 @@ public class KCMSteamManager : MonoBehaviour {
|
||||
|
||||
protected virtual void Awake() {
|
||||
// Only one instance of SteamManager at a time!
|
||||
Main.helper.Log("Steam awake");
|
||||
Main.Log("Steam awake");
|
||||
if (s_instance != null) {
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
@@ -76,7 +76,7 @@ public class KCMSteamManager : MonoBehaviour {
|
||||
// The most common case where this happens is when SteamManager gets destroyed because of Application.Quit(),
|
||||
// and then some Steamworks code in some other OnDestroy gets called afterwards, creating a new SteamManager.
|
||||
// You should never call Steamworks functions in OnDestroy, always prefer OnDisable if possible.
|
||||
Main.helper.Log("Tried to Initialize the SteamAPI twice in one session!");
|
||||
Main.Log("Tried to Initialize the SteamAPI twice in one session!");
|
||||
|
||||
return;
|
||||
}
|
||||
@@ -85,11 +85,11 @@ public class KCMSteamManager : MonoBehaviour {
|
||||
DontDestroyOnLoad(gameObject);
|
||||
|
||||
if (!Packsize.Test()) {
|
||||
Main.helper.Log("[Steamworks.NET] Packsize Test returned false, the wrong version of Steamworks.NET is being run in this platform.");
|
||||
Main.Log("[Steamworks.NET] Packsize Test returned false, the wrong version of Steamworks.NET is being run in this platform.");
|
||||
}
|
||||
|
||||
if (!DllCheck.Test()) {
|
||||
Main.helper.Log("[Steamworks.NET] DllCheck Test returned false, One or more of the Steamworks binaries seems to be the wrong version.");
|
||||
Main.Log("[Steamworks.NET] DllCheck Test returned false, One or more of the Steamworks binaries seems to be the wrong version.");
|
||||
}
|
||||
|
||||
try {
|
||||
@@ -101,12 +101,12 @@ public class KCMSteamManager : MonoBehaviour {
|
||||
// See the Valve documentation for more information: https://partner.steamgames.com/doc/sdk/api#initialization_and_shutdown
|
||||
if (SteamAPI.RestartAppIfNecessary((AppId_t)569480)) {
|
||||
//Application.Quit();
|
||||
Main.helper.Log("Attempted to restart app");
|
||||
Main.Log("Attempted to restart app");
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch (System.DllNotFoundException e) { // We catch this exception here, as it will be the first occurrence of it.
|
||||
Main.helper.Log("[Steamworks.NET] Could not load [lib]steam_api.dll/so/dylib. It's likely not in the correct location. Refer to the README for more details.\n" + e);
|
||||
Main.Log("[Steamworks.NET] Could not load [lib]steam_api.dll/so/dylib. It's likely not in the correct location. Refer to the README for more details.\n" + e);
|
||||
|
||||
//Application.Quit();
|
||||
return;
|
||||
@@ -123,7 +123,7 @@ public class KCMSteamManager : MonoBehaviour {
|
||||
// https://partner.steamgames.com/doc/sdk/api#initialization_and_shutdown
|
||||
m_bInitialized = SteamAPI.Init();
|
||||
if (!m_bInitialized) {
|
||||
Main.helper.Log("[Steamworks.NET] SteamAPI_Init() failed. Refer to Valve's documentation or the comment above this line for more information.");
|
||||
Main.Log("[Steamworks.NET] SteamAPI_Init() failed. Refer to Valve's documentation or the comment above this line for more information.");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using KCM;
|
||||
using KCM.Enums;
|
||||
using KCM.Packets.Handlers;
|
||||
using KCM.StateManagement.Observers;
|
||||
using Steamworks;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -153,20 +154,7 @@ namespace Riptide.Demos.Steam.PlayerHosted
|
||||
//NetworkManager.Singleton.StopServer();
|
||||
//NetworkManager.Singleton.DisconnectClient();
|
||||
SteamMatchmaking.LeaveLobby(lobbyId);
|
||||
|
||||
if (KCClient.client.IsConnected)
|
||||
KCClient.client.Disconnect();
|
||||
|
||||
Main.helper.Log("clear players");
|
||||
Main.kCPlayers.Clear();
|
||||
LobbyHandler.ClearPlayerList();
|
||||
LobbyHandler.ClearChatEntries();
|
||||
Main.helper.Log("end clear players");
|
||||
|
||||
if (KCServer.IsRunning)
|
||||
KCServer.server.Stop();
|
||||
|
||||
|
||||
Main.ResetMultiplayerState("LeaveLobby");
|
||||
|
||||
Main.TransitionTo(MenuState.ServerBrowser);
|
||||
ServerBrowser.registerServer = false;
|
||||
|
||||
@@ -299,35 +299,13 @@ namespace KCM
|
||||
|
||||
try
|
||||
{
|
||||
if (Constants.MainMenuUI_T == null)
|
||||
{
|
||||
Main.helper.Log("MainMenuUI_T is null in ServerBrowser");
|
||||
return;
|
||||
}
|
||||
|
||||
var topLevelCanvas = ResolveMenuCanvas();
|
||||
if (topLevelCanvas == null)
|
||||
{
|
||||
Main.helper.Log("Failed to resolve top-level menu canvas in ServerBrowser");
|
||||
return;
|
||||
}
|
||||
|
||||
GameObject kcmUICanvas = Instantiate(topLevelCanvas.gameObject);
|
||||
GameObject kcmUICanvas = Instantiate(Constants.MainMenuUI_T.Find("TopLevelUICanvas").gameObject);
|
||||
|
||||
for (int i = 0; i < kcmUICanvas.transform.childCount; i++)
|
||||
Destroy(kcmUICanvas.transform.GetChild(i).gameObject);
|
||||
|
||||
kcmUICanvas.name = "KCMUICanvas";
|
||||
kcmUICanvas.transform.SetParent(Constants.MainMenuUI_T, false);
|
||||
kcmUICanvas.transform.SetAsLastSibling();
|
||||
kcmUICanvas.SetActive(false);
|
||||
|
||||
var canvasComponent = kcmUICanvas.GetComponent<Canvas>();
|
||||
if (canvasComponent != null)
|
||||
{
|
||||
canvasComponent.overrideSorting = true;
|
||||
canvasComponent.sortingOrder = 999;
|
||||
}
|
||||
kcmUICanvas.transform.SetParent(Constants.MainMenuUI_T);
|
||||
|
||||
KCMUICanvas = kcmUICanvas.transform;
|
||||
|
||||
@@ -344,8 +322,6 @@ namespace KCM
|
||||
serverLobbyPlayerRef = serverLobbyRef.transform.Find("Container/PlayerList/Viewport/Content");
|
||||
serverLobbyChatRef = serverLobbyRef.transform.Find("Container/PlayerChat/Viewport/Content");
|
||||
serverLobbyRef.SetActive(false);
|
||||
serverBrowserRef.transform.SetAsLastSibling();
|
||||
serverLobbyRef.transform.SetAsLastSibling();
|
||||
//browser.transform.position = new Vector3(0, 0, 0);
|
||||
|
||||
|
||||
@@ -424,7 +400,7 @@ namespace KCM
|
||||
SfxSystem.PlayUiSelect();
|
||||
|
||||
|
||||
Main.TransitionTo(MenuState.Menu);
|
||||
Main.TransitionTo(KCM.Enums.MenuState.Menu);
|
||||
});
|
||||
|
||||
|
||||
@@ -459,29 +435,6 @@ namespace KCM
|
||||
}
|
||||
}
|
||||
|
||||
private Transform ResolveMenuCanvas()
|
||||
{
|
||||
string[] candidatePaths =
|
||||
{
|
||||
"TopLevelUICanvas",
|
||||
"TopLevel",
|
||||
"MainMenu/TopLevel/TopLevelUICanvas",
|
||||
"MainMenu/TopLevel"
|
||||
};
|
||||
|
||||
foreach (var path in candidatePaths)
|
||||
{
|
||||
var transform = Constants.MainMenuUI_T.Find(path);
|
||||
if (transform != null)
|
||||
{
|
||||
Main.helper.Log($"ServerBrowser: using canvas path '{path}'.");
|
||||
return transform;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private void Preload(KCModHelper helper)
|
||||
{
|
||||
helper.Log("Hello?");
|
||||
|
||||
@@ -51,12 +51,24 @@ namespace KCM.ServerLobby.LobbyChat
|
||||
{
|
||||
try
|
||||
{
|
||||
KCPlayer player;
|
||||
Main.kCPlayers.TryGetValue(Main.GetPlayerByClientID(Client).steamId, out player);
|
||||
if (banner == null)
|
||||
return;
|
||||
|
||||
var bannerTexture = World.inst.liverySets[player.banner].banners;
|
||||
|
||||
banner.texture = bannerTexture;
|
||||
string steamId;
|
||||
if (!Main.clientSteamIds.TryGetValue(Client, out steamId))
|
||||
return;
|
||||
|
||||
KCPlayer player;
|
||||
if (!Main.kCPlayers.TryGetValue(steamId, out player) || player == null)
|
||||
return;
|
||||
|
||||
if (World.inst == null || World.inst.liverySets == null)
|
||||
return;
|
||||
|
||||
if (player.banner < 0 || player.banner >= World.inst.liverySets.Count)
|
||||
return;
|
||||
|
||||
banner.texture = World.inst.liverySets[player.banner].banners;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -29,7 +29,7 @@ namespace KCM.ServerLobby
|
||||
|
||||
transform.Find("PlayerBanner").GetComponent<Button>().onClick.AddListener(() =>
|
||||
{
|
||||
Main.TransitionTo(MenuState.NameAndBanner);//ChooseBannerUI Hooks required, as well as townnameui
|
||||
Main.TransitionTo(MenuState.NameAndBanner);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -37,21 +37,39 @@ namespace KCM.ServerLobby
|
||||
{
|
||||
try
|
||||
{
|
||||
// First check if the client still exists
|
||||
if (!Main.TryGetPlayerByClientID(Client, out KCPlayer player) || player == null)
|
||||
if (banner == null)
|
||||
{
|
||||
// Client no longer exists, stop the repeating invoke and destroy this entry
|
||||
CancelInvoke("SetValues");
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
var bannerTransform = transform.Find("PlayerBanner");
|
||||
if (bannerTransform == null)
|
||||
return;
|
||||
banner = bannerTransform.GetComponent<RawImage>();
|
||||
if (banner == null)
|
||||
return;
|
||||
}
|
||||
|
||||
transform.Find("PlayerName").GetComponent<TextMeshProUGUI>().text = player.name;
|
||||
transform.Find("Ready").gameObject.SetActive(player.ready);
|
||||
string steamId;
|
||||
if (!Main.clientSteamIds.TryGetValue(Client, out steamId))
|
||||
return;
|
||||
|
||||
var bannerTexture = World.inst.liverySets[player.banner].banners;
|
||||
KCPlayer player;
|
||||
if (!Main.kCPlayers.TryGetValue(steamId, out player) || player == null)
|
||||
return;
|
||||
|
||||
banner.texture = bannerTexture;
|
||||
var nameTransform = transform.Find("PlayerName");
|
||||
if (nameTransform != null)
|
||||
nameTransform.GetComponent<TextMeshProUGUI>().text = player.name ?? "";
|
||||
|
||||
var readyTransform = transform.Find("Ready");
|
||||
if (readyTransform != null)
|
||||
readyTransform.gameObject.SetActive(player.ready);
|
||||
|
||||
if (World.inst == null || World.inst.liverySets == null)
|
||||
return;
|
||||
|
||||
if (player.banner < 0 || player.banner >= World.inst.liverySets.Count)
|
||||
return;
|
||||
|
||||
banner.texture = World.inst.liverySets[player.banner].banners;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -142,6 +142,7 @@ namespace KCM
|
||||
{
|
||||
|
||||
Main.helper.Log("Disable all");
|
||||
//StartGameButton.gameObject.SetActive(false);
|
||||
StartGameButton.onClick.RemoveAllListeners();
|
||||
StartGameButton.GetComponentInChildren<TextMeshProUGUI>().text = "Ready";
|
||||
StartGameButton.onClick.AddListener(() =>
|
||||
@@ -186,32 +187,6 @@ namespace KCM
|
||||
StartGameButton.GetComponentInChildren<TextMeshProUGUI>().text = "Start";
|
||||
StartGameButton.onClick.AddListener(() =>
|
||||
{
|
||||
int definitiveSeed;
|
||||
if (string.IsNullOrWhiteSpace(WorldSeed.text))
|
||||
{
|
||||
World.inst.Generate();
|
||||
definitiveSeed = World.inst.seed;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (int.TryParse(WorldSeed.text, out int parsedSeed))
|
||||
{
|
||||
definitiveSeed = parsedSeed;
|
||||
World.inst.Generate(definitiveSeed);
|
||||
}
|
||||
else
|
||||
{
|
||||
Main.helper.Log($"Invalid seed '{WorldSeed.text}' entered. Generating a random seed.");
|
||||
World.inst.Generate();
|
||||
definitiveSeed = World.inst.seed;
|
||||
}
|
||||
}
|
||||
|
||||
new WorldSeed()
|
||||
{
|
||||
Seed = definitiveSeed
|
||||
}.SendToAll(KCClient.client.Id);
|
||||
|
||||
new StartGame().SendToAll();
|
||||
|
||||
if (PlacementType.value == 0 && !LobbyManager.loadingSave)
|
||||
@@ -244,6 +219,21 @@ namespace KCM
|
||||
{
|
||||
if (ChatInput.text.Length > 0)
|
||||
{
|
||||
if (ChatInput.text.Trim().Equals("/resync", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
try
|
||||
{
|
||||
new KCM.Packets.Network.ResyncRequestPacket { reason = "manual:/resync" }.Send();
|
||||
LobbyHandler.AddSystemMessage("Resync requested.");
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
ChatInput.text = "";
|
||||
return;
|
||||
}
|
||||
|
||||
new ChatMessage()
|
||||
{
|
||||
PlayerName = KCClient.inst.Name,
|
||||
|
||||
@@ -13,8 +13,6 @@ namespace KCM.StateManagement.BuildingState
|
||||
{
|
||||
public class BuildingStateManager
|
||||
{
|
||||
private static readonly Dictionary<Guid, float> lastUpdateTime = new Dictionary<Guid, float>();
|
||||
private const float UpdateInterval = 0.1f; // 10 times per second
|
||||
|
||||
public static void BuildingStateChanged(object sender, StateUpdateEventArgs args)
|
||||
{
|
||||
@@ -26,29 +24,9 @@ namespace KCM.StateManagement.BuildingState
|
||||
try
|
||||
{
|
||||
Observer observer = (Observer)sender;
|
||||
|
||||
Building building = (Building)observer.state;
|
||||
|
||||
if (building == null)
|
||||
{
|
||||
if(observer != null)
|
||||
{
|
||||
UnityEngine.Object.Destroy(observer.gameObject);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Guid guid = building.guid;
|
||||
|
||||
if (lastUpdateTime.ContainsKey(guid) && Time.time < lastUpdateTime[guid] + UpdateInterval)
|
||||
{
|
||||
return; // Not time to update yet
|
||||
}
|
||||
|
||||
if (!lastUpdateTime.ContainsKey(guid))
|
||||
lastUpdateTime.Add(guid, Time.time);
|
||||
else
|
||||
lastUpdateTime[guid] = Time.time;
|
||||
|
||||
//Main.helper.Log("Should send building network update for: " + building.UniqueName);
|
||||
|
||||
new BuildingStatePacket()
|
||||
@@ -80,4 +58,4 @@ namespace KCM.StateManagement.BuildingState
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,24 @@ namespace KCM.StateManagement.Observers
|
||||
{
|
||||
public static Dictionary<int, IObserver> observers = new Dictionary<int, IObserver>();
|
||||
|
||||
public static void ClearAll()
|
||||
{
|
||||
foreach (var observer in observers.Values)
|
||||
{
|
||||
try
|
||||
{
|
||||
var component = observer as Component;
|
||||
if (component != null)
|
||||
UnityEngine.Object.Destroy(component.gameObject);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
observers.Clear();
|
||||
}
|
||||
|
||||
public static void RegisterObserver<T>(T instance, string[] monitoredFields, EventHandler<StateUpdateEventArgs> eventHandler = null, EventHandler<StateUpdateEventArgs> sendUpdateHandler = null)
|
||||
{
|
||||
if (observers.ContainsKey(instance.GetHashCode()))
|
||||
|
||||
836
StateManagement/Sync/SyncManager.cs
Normal file
836
StateManagement/Sync/SyncManager.cs
Normal file
@@ -0,0 +1,836 @@
|
||||
using Assets.Code;
|
||||
using KCM.Packets.Game.GameVillager;
|
||||
using KCM.Packets.State;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
|
||||
namespace KCM.StateManagement.Sync
|
||||
{
|
||||
public static class SyncManager
|
||||
{
|
||||
private const int ResourceBroadcastIntervalMs = 2000;
|
||||
private const int MaxBuildingSnapshotBytes = 30000;
|
||||
private const int MaxVillagerTeleportsPerResync = 400;
|
||||
private const int VillagerValidationIntervalMs = 10000; // 10 seconds
|
||||
|
||||
private static long lastResourceBroadcastMs;
|
||||
private static long lastVillagerValidationMs;
|
||||
|
||||
private static FieldInfo freeResourceAmountField;
|
||||
private static MethodInfo resourceAmountGetMethod;
|
||||
private static MethodInfo resourceAmountSetMethod;
|
||||
private static MethodInfo freeResourceManagerMaybeRefresh;
|
||||
private static MethodInfo fieldSystemMaybeRefresh;
|
||||
|
||||
public static void ServerUpdate()
|
||||
{
|
||||
if (!KCServer.IsRunning)
|
||||
return;
|
||||
|
||||
long now = DateTimeOffset.Now.ToUnixTimeMilliseconds();
|
||||
|
||||
// Resource broadcast
|
||||
if ((now - lastResourceBroadcastMs) >= ResourceBroadcastIntervalMs)
|
||||
{
|
||||
lastResourceBroadcastMs = now;
|
||||
|
||||
try
|
||||
{
|
||||
ResourceSnapshotPacket snapshot = BuildResourceSnapshotPacket();
|
||||
if (snapshot == null)
|
||||
return;
|
||||
|
||||
snapshot.clientId = KCClient.client != null ? KCClient.client.Id : (ushort)0;
|
||||
|
||||
// Exclude host/local client from receiving its own snapshot.
|
||||
ushort exceptId = KCClient.client != null ? KCClient.client.Id : (ushort)0;
|
||||
snapshot.SendToAll(exceptId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("Error broadcasting resource snapshot");
|
||||
Main.helper.Log(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
// Villager state validation
|
||||
if ((now - lastVillagerValidationMs) >= VillagerValidationIntervalMs)
|
||||
{
|
||||
lastVillagerValidationMs = now;
|
||||
ValidateAndCorrectVillagerStates();
|
||||
}
|
||||
}
|
||||
|
||||
public static void SendResyncToClient(ushort toClient, string reason)
|
||||
{
|
||||
if (!KCServer.IsRunning)
|
||||
return;
|
||||
|
||||
Main.helper.Log($"Resync requested by client {toClient} ({reason ?? ""})");
|
||||
|
||||
try
|
||||
{
|
||||
ResourceSnapshotPacket snapshot = BuildResourceSnapshotPacket();
|
||||
if (snapshot != null)
|
||||
{
|
||||
snapshot.clientId = KCClient.client != null ? KCClient.client.Id : (ushort)0;
|
||||
snapshot.Send(toClient);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("Error sending resource resync");
|
||||
Main.helper.Log(ex.ToString());
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
SendBuildingSnapshotToClient(toClient);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("Error sending building resync");
|
||||
Main.helper.Log(ex.ToString());
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
SendVillagerTeleportSnapshotToClient(toClient);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("Error sending villager resync");
|
||||
Main.helper.Log(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
private static ResourceSnapshotPacket BuildResourceSnapshotPacket()
|
||||
{
|
||||
List<int> types;
|
||||
List<int> amounts;
|
||||
if (!TryReadFreeResources(out types, out amounts))
|
||||
return null;
|
||||
|
||||
return new ResourceSnapshotPacket
|
||||
{
|
||||
resourceTypes = types,
|
||||
amounts = amounts
|
||||
};
|
||||
}
|
||||
|
||||
private static void SendBuildingSnapshotToClient(ushort toClient)
|
||||
{
|
||||
List<Building> buildings = new List<Building>();
|
||||
|
||||
foreach (var p in Main.kCPlayers.Values)
|
||||
{
|
||||
if (p == null || p.inst == null)
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
var list = p.inst.Buildings;
|
||||
for (int i = 0; i < list.Count; i++)
|
||||
{
|
||||
Building b = list.data[i];
|
||||
if (b != null)
|
||||
buildings.Add(b);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
if (buildings.Count == 0)
|
||||
return;
|
||||
|
||||
int idx = 0;
|
||||
while (idx < buildings.Count)
|
||||
{
|
||||
byte[] payload = BuildBuildingSnapshotPayload(buildings, ref idx);
|
||||
if (payload == null || payload.Length == 0)
|
||||
break;
|
||||
|
||||
new BuildingSnapshotPacket { payload = payload }.Send(toClient);
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] BuildBuildingSnapshotPayload(List<Building> buildings, ref int startIndex)
|
||||
{
|
||||
byte[] buffer = new byte[MaxBuildingSnapshotBytes];
|
||||
int offset = 0;
|
||||
int countOffset = offset;
|
||||
if (!TryWriteInt32(buffer, ref offset, 0))
|
||||
return new byte[0];
|
||||
|
||||
int written = 0;
|
||||
for (; startIndex < buildings.Count; startIndex++)
|
||||
{
|
||||
Building b = buildings[startIndex];
|
||||
if (b == null)
|
||||
continue;
|
||||
|
||||
int before = offset;
|
||||
if (!TryWriteBuildingRecord(buffer, ref offset, b))
|
||||
{
|
||||
offset = before;
|
||||
startIndex++;
|
||||
break;
|
||||
}
|
||||
|
||||
written++;
|
||||
|
||||
if (offset >= MaxBuildingSnapshotBytes - 256)
|
||||
{
|
||||
startIndex++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
WriteInt32At(buffer, countOffset, written);
|
||||
byte[] result = new byte[offset];
|
||||
Buffer.BlockCopy(buffer, 0, result, 0, offset);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool TryWriteBuildingRecord(byte[] buffer, ref int offset, Building b)
|
||||
{
|
||||
if (!TryWriteInt32(buffer, ref offset, b.TeamID()))
|
||||
return false;
|
||||
if (!TryWriteGuidBytes(buffer, ref offset, b.guid))
|
||||
return false;
|
||||
|
||||
if (!TryWriteString(buffer, ref offset, b.UniqueName ?? ""))
|
||||
return false;
|
||||
if (!TryWriteString(buffer, ref offset, b.customName ?? ""))
|
||||
return false;
|
||||
|
||||
Vector3 globalPosition = b.transform.position;
|
||||
Quaternion rotation = b.transform.childCount > 0 ? b.transform.GetChild(0).rotation : b.transform.rotation;
|
||||
Vector3 localPosition = b.transform.childCount > 0 ? b.transform.GetChild(0).localPosition : Vector3.zero;
|
||||
|
||||
if (!TryWriteSingle(buffer, ref offset, globalPosition.x) ||
|
||||
!TryWriteSingle(buffer, ref offset, globalPosition.y) ||
|
||||
!TryWriteSingle(buffer, ref offset, globalPosition.z))
|
||||
return false;
|
||||
|
||||
if (!TryWriteSingle(buffer, ref offset, rotation.x) ||
|
||||
!TryWriteSingle(buffer, ref offset, rotation.y) ||
|
||||
!TryWriteSingle(buffer, ref offset, rotation.z) ||
|
||||
!TryWriteSingle(buffer, ref offset, rotation.w))
|
||||
return false;
|
||||
|
||||
if (!TryWriteSingle(buffer, ref offset, localPosition.x) ||
|
||||
!TryWriteSingle(buffer, ref offset, localPosition.y) ||
|
||||
!TryWriteSingle(buffer, ref offset, localPosition.z))
|
||||
return false;
|
||||
|
||||
if (!TryWriteBool(buffer, ref offset, b.IsBuilt()) ||
|
||||
!TryWriteBool(buffer, ref offset, b.IsPlaced()) ||
|
||||
!TryWriteBool(buffer, ref offset, b.Open) ||
|
||||
!TryWriteBool(buffer, ref offset, b.doBuildAnimation) ||
|
||||
!TryWriteBool(buffer, ref offset, b.constructionPaused))
|
||||
return false;
|
||||
|
||||
if (!TryWriteSingle(buffer, ref offset, b.constructionProgress))
|
||||
return false;
|
||||
|
||||
float resourceProgress = 0f;
|
||||
try
|
||||
{
|
||||
var field = b.GetType().GetField("resourceProgress", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
if (field != null)
|
||||
resourceProgress = (float)field.GetValue(b);
|
||||
}
|
||||
catch { }
|
||||
if (!TryWriteSingle(buffer, ref offset, resourceProgress))
|
||||
return false;
|
||||
|
||||
if (!TryWriteSingle(buffer, ref offset, b.Life) ||
|
||||
!TryWriteSingle(buffer, ref offset, b.ModifiedMaxLife))
|
||||
return false;
|
||||
|
||||
int yearBuilt = 0;
|
||||
try
|
||||
{
|
||||
var field = b.GetType().GetField("yearBuilt", BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
if (field != null)
|
||||
yearBuilt = (int)field.GetValue(b);
|
||||
}
|
||||
catch { }
|
||||
if (!TryWriteInt32(buffer, ref offset, yearBuilt))
|
||||
return false;
|
||||
|
||||
if (!TryWriteSingle(buffer, ref offset, b.decayProtection))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void ApplyBuildingSnapshot(byte[] payload)
|
||||
{
|
||||
if (payload == null || payload.Length < 4)
|
||||
return;
|
||||
|
||||
int offset = 0;
|
||||
int count;
|
||||
if (!TryReadInt32(payload, ref offset, out count))
|
||||
return;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
int teamId;
|
||||
Guid guid;
|
||||
string uniqueName;
|
||||
string customName;
|
||||
Vector3 globalPosition;
|
||||
Quaternion rotation;
|
||||
Vector3 localPosition;
|
||||
bool built;
|
||||
bool placed;
|
||||
bool open;
|
||||
bool doBuildAnimation;
|
||||
bool constructionPaused;
|
||||
float constructionProgress;
|
||||
float resourceProgress;
|
||||
float life;
|
||||
float modifiedMaxLife;
|
||||
int yearBuilt;
|
||||
float decayProtection;
|
||||
|
||||
if (!TryReadInt32(payload, ref offset, out teamId))
|
||||
break;
|
||||
if (!TryReadGuid(payload, ref offset, out guid))
|
||||
break;
|
||||
if (!TryReadString(payload, ref offset, out uniqueName))
|
||||
break;
|
||||
if (!TryReadString(payload, ref offset, out customName))
|
||||
break;
|
||||
|
||||
float gx, gy, gz;
|
||||
float rx, ry, rz, rw;
|
||||
float lx, ly, lz;
|
||||
if (!TryReadSingle(payload, ref offset, out gx) ||
|
||||
!TryReadSingle(payload, ref offset, out gy) ||
|
||||
!TryReadSingle(payload, ref offset, out gz))
|
||||
break;
|
||||
globalPosition = new Vector3(gx, gy, gz);
|
||||
|
||||
if (!TryReadSingle(payload, ref offset, out rx) ||
|
||||
!TryReadSingle(payload, ref offset, out ry) ||
|
||||
!TryReadSingle(payload, ref offset, out rz) ||
|
||||
!TryReadSingle(payload, ref offset, out rw))
|
||||
break;
|
||||
rotation = new Quaternion(rx, ry, rz, rw);
|
||||
|
||||
if (!TryReadSingle(payload, ref offset, out lx) ||
|
||||
!TryReadSingle(payload, ref offset, out ly) ||
|
||||
!TryReadSingle(payload, ref offset, out lz))
|
||||
break;
|
||||
localPosition = new Vector3(lx, ly, lz);
|
||||
|
||||
if (!TryReadBool(payload, ref offset, out built) ||
|
||||
!TryReadBool(payload, ref offset, out placed) ||
|
||||
!TryReadBool(payload, ref offset, out open) ||
|
||||
!TryReadBool(payload, ref offset, out doBuildAnimation) ||
|
||||
!TryReadBool(payload, ref offset, out constructionPaused))
|
||||
break;
|
||||
|
||||
if (!TryReadSingle(payload, ref offset, out constructionProgress) ||
|
||||
!TryReadSingle(payload, ref offset, out resourceProgress) ||
|
||||
!TryReadSingle(payload, ref offset, out life) ||
|
||||
!TryReadSingle(payload, ref offset, out modifiedMaxLife) ||
|
||||
!TryReadInt32(payload, ref offset, out yearBuilt) ||
|
||||
!TryReadSingle(payload, ref offset, out decayProtection))
|
||||
break;
|
||||
|
||||
ApplyBuildingRecord(teamId, guid, uniqueName, customName, globalPosition, rotation, localPosition, built, placed, open, doBuildAnimation, constructionPaused, constructionProgress, resourceProgress, life, modifiedMaxLife, yearBuilt, decayProtection);
|
||||
}
|
||||
|
||||
TryRefreshFieldSystem();
|
||||
}
|
||||
|
||||
private static bool EnsureCapacity(byte[] buffer, int offset, int bytesToWrite)
|
||||
{
|
||||
return buffer != null && offset >= 0 && (offset + bytesToWrite) <= buffer.Length;
|
||||
}
|
||||
|
||||
private static bool TryWriteInt32(byte[] buffer, ref int offset, int value)
|
||||
{
|
||||
if (!EnsureCapacity(buffer, offset, 4))
|
||||
return false;
|
||||
byte[] bytes = BitConverter.GetBytes(value);
|
||||
Buffer.BlockCopy(bytes, 0, buffer, offset, 4);
|
||||
offset += 4;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void WriteInt32At(byte[] buffer, int offset, int value)
|
||||
{
|
||||
if (!EnsureCapacity(buffer, offset, 4))
|
||||
return;
|
||||
byte[] bytes = BitConverter.GetBytes(value);
|
||||
Buffer.BlockCopy(bytes, 0, buffer, offset, 4);
|
||||
}
|
||||
|
||||
private static bool TryWriteSingle(byte[] buffer, ref int offset, float value)
|
||||
{
|
||||
if (!EnsureCapacity(buffer, offset, 4))
|
||||
return false;
|
||||
byte[] bytes = BitConverter.GetBytes(value);
|
||||
Buffer.BlockCopy(bytes, 0, buffer, offset, 4);
|
||||
offset += 4;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteBool(byte[] buffer, ref int offset, bool value)
|
||||
{
|
||||
if (!EnsureCapacity(buffer, offset, 1))
|
||||
return false;
|
||||
buffer[offset++] = (byte)(value ? 1 : 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteGuidBytes(byte[] buffer, ref int offset, Guid guid)
|
||||
{
|
||||
if (!EnsureCapacity(buffer, offset, 16))
|
||||
return false;
|
||||
byte[] bytes = guid.ToByteArray();
|
||||
Buffer.BlockCopy(bytes, 0, buffer, offset, 16);
|
||||
offset += 16;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteString(byte[] buffer, ref int offset, string value)
|
||||
{
|
||||
if (value == null)
|
||||
value = "";
|
||||
|
||||
byte[] bytes = Encoding.UTF8.GetBytes(value);
|
||||
if (!TryWriteInt32(buffer, ref offset, bytes.Length))
|
||||
return false;
|
||||
if (!EnsureCapacity(buffer, offset, bytes.Length))
|
||||
return false;
|
||||
Buffer.BlockCopy(bytes, 0, buffer, offset, bytes.Length);
|
||||
offset += bytes.Length;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadInt32(byte[] buffer, ref int offset, out int value)
|
||||
{
|
||||
value = 0;
|
||||
if (!EnsureCapacity(buffer, offset, 4))
|
||||
return false;
|
||||
value = BitConverter.ToInt32(buffer, offset);
|
||||
offset += 4;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadSingle(byte[] buffer, ref int offset, out float value)
|
||||
{
|
||||
value = 0f;
|
||||
if (!EnsureCapacity(buffer, offset, 4))
|
||||
return false;
|
||||
value = BitConverter.ToSingle(buffer, offset);
|
||||
offset += 4;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadBool(byte[] buffer, ref int offset, out bool value)
|
||||
{
|
||||
value = false;
|
||||
if (!EnsureCapacity(buffer, offset, 1))
|
||||
return false;
|
||||
value = buffer[offset++] != 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadGuid(byte[] buffer, ref int offset, out Guid value)
|
||||
{
|
||||
value = Guid.Empty;
|
||||
if (!EnsureCapacity(buffer, offset, 16))
|
||||
return false;
|
||||
byte[] bytes = new byte[16];
|
||||
Buffer.BlockCopy(buffer, offset, bytes, 0, 16);
|
||||
offset += 16;
|
||||
value = new Guid(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadString(byte[] buffer, ref int offset, out string value)
|
||||
{
|
||||
value = "";
|
||||
int len;
|
||||
if (!TryReadInt32(buffer, ref offset, out len))
|
||||
return false;
|
||||
if (len < 0 || !EnsureCapacity(buffer, offset, len))
|
||||
return false;
|
||||
value = len == 0 ? "" : Encoding.UTF8.GetString(buffer, offset, len);
|
||||
offset += len;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void ApplyBuildingRecord(int teamId, Guid guid, string uniqueName, string customName, Vector3 globalPosition, Quaternion rotation, Vector3 localPosition, bool built, bool placed, bool open, bool doBuildAnimation, bool constructionPaused, float constructionProgress, float resourceProgress, float life, float modifiedMaxLife, int yearBuilt, float decayProtection)
|
||||
{
|
||||
Player p = Main.GetPlayerByTeamID(teamId);
|
||||
if (p == null)
|
||||
return;
|
||||
|
||||
Building building = null;
|
||||
try { building = p.GetBuilding(guid); } catch { }
|
||||
if (building == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
building.UniqueName = uniqueName;
|
||||
building.customName = customName;
|
||||
|
||||
building.transform.position = globalPosition;
|
||||
if (building.transform.childCount > 0)
|
||||
{
|
||||
building.transform.GetChild(0).rotation = rotation;
|
||||
building.transform.GetChild(0).localPosition = localPosition;
|
||||
}
|
||||
else
|
||||
{
|
||||
building.transform.rotation = rotation;
|
||||
}
|
||||
|
||||
SetPrivateFieldValue(building, "built", built);
|
||||
SetPrivateFieldValue(building, "placed", placed);
|
||||
SetPrivateFieldValue(building, "resourceProgress", resourceProgress);
|
||||
SetPrivateFieldValue(building, "yearBuilt", yearBuilt);
|
||||
|
||||
building.Open = open;
|
||||
building.doBuildAnimation = doBuildAnimation;
|
||||
building.constructionPaused = constructionPaused;
|
||||
building.constructionProgress = constructionProgress;
|
||||
building.Life = life;
|
||||
building.ModifiedMaxLife = modifiedMaxLife;
|
||||
building.decayProtection = decayProtection;
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private static void SetPrivateFieldValue(object obj, string fieldName, object value)
|
||||
{
|
||||
if (obj == null)
|
||||
return;
|
||||
|
||||
Type type = obj.GetType();
|
||||
FieldInfo field = type.GetField(fieldName, BindingFlags.NonPublic | BindingFlags.Instance);
|
||||
if (field != null)
|
||||
field.SetValue(obj, value);
|
||||
}
|
||||
|
||||
private static void SendVillagerTeleportSnapshotToClient(ushort toClient)
|
||||
{
|
||||
try
|
||||
{
|
||||
int sent = 0;
|
||||
for (int i = 0; i < Villager.villagers.Count; i++)
|
||||
{
|
||||
if (sent >= MaxVillagerTeleportsPerResync)
|
||||
break;
|
||||
|
||||
Villager v = Villager.villagers.data[i];
|
||||
if (v == null)
|
||||
continue;
|
||||
|
||||
new VillagerTeleportTo
|
||||
{
|
||||
guid = v.guid,
|
||||
pos = v.Pos
|
||||
}.Send(toClient);
|
||||
|
||||
sent++;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public static void ApplyResourceSnapshot(List<int> resourceTypes, List<int> amounts)
|
||||
{
|
||||
if (resourceTypes == null || amounts == null)
|
||||
return;
|
||||
|
||||
int count = Math.Min(resourceTypes.Count, amounts.Count);
|
||||
if (count == 0)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
FreeResourceType type = (FreeResourceType)resourceTypes[i];
|
||||
int amount = amounts[i];
|
||||
TryWriteFreeResource(type, amount);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
TryRefreshFreeResourceUI();
|
||||
}
|
||||
|
||||
private static bool TryReadFreeResources(out List<int> types, out List<int> amounts)
|
||||
{
|
||||
types = new List<int>();
|
||||
amounts = new List<int>();
|
||||
|
||||
if (FreeResourceManager.inst == null)
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
Array values = Enum.GetValues(typeof(FreeResourceType));
|
||||
foreach (var v in values)
|
||||
{
|
||||
FreeResourceType t = (FreeResourceType)v;
|
||||
int amount;
|
||||
if (!TryReadFreeResource(t, out amount))
|
||||
continue;
|
||||
|
||||
types.Add((int)t);
|
||||
amounts.Add(amount);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return types.Count > 0;
|
||||
}
|
||||
|
||||
private static bool EnsureResourceReflection()
|
||||
{
|
||||
if (resourceAmountGetMethod != null && resourceAmountSetMethod != null && freeResourceAmountField != null)
|
||||
return true;
|
||||
|
||||
try
|
||||
{
|
||||
Type raType = typeof(ResourceAmount);
|
||||
resourceAmountGetMethod = raType.GetMethod("Get", BindingFlags.Public | BindingFlags.Instance, null, new[] { typeof(FreeResourceType) }, null);
|
||||
resourceAmountSetMethod = raType.GetMethod("Set", BindingFlags.Public | BindingFlags.Instance, null, new[] { typeof(FreeResourceType), typeof(int) }, null);
|
||||
|
||||
var bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
|
||||
freeResourceAmountField = typeof(FreeResourceManager).GetFields(bindingFlags).FirstOrDefault(f => f.FieldType == raType);
|
||||
if (freeResourceAmountField == null)
|
||||
{
|
||||
var prop = typeof(FreeResourceManager).GetProperties(bindingFlags).FirstOrDefault(p => p.PropertyType == raType && p.GetGetMethod(true) != null);
|
||||
if (prop != null)
|
||||
{
|
||||
// Fallback: treat property getter as "field" by caching getter only.
|
||||
// We won't be able to set back reliably in this case.
|
||||
}
|
||||
}
|
||||
|
||||
freeResourceManagerMaybeRefresh = typeof(FreeResourceManager).GetMethods(bindingFlags)
|
||||
.FirstOrDefault(m => m.GetParameters().Length == 0 && m.ReturnType == typeof(void) && (m.Name.IndexOf("Refresh", StringComparison.OrdinalIgnoreCase) >= 0 || m.Name.IndexOf("Update", StringComparison.OrdinalIgnoreCase) >= 0));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return freeResourceAmountField != null && resourceAmountGetMethod != null && resourceAmountSetMethod != null;
|
||||
}
|
||||
|
||||
private static bool TryReadFreeResource(FreeResourceType type, out int amount)
|
||||
{
|
||||
amount = 0;
|
||||
|
||||
if (!EnsureResourceReflection())
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
object ra = freeResourceAmountField.GetValue(FreeResourceManager.inst);
|
||||
if (ra == null)
|
||||
return false;
|
||||
|
||||
object result = resourceAmountGetMethod.Invoke(ra, new object[] { type });
|
||||
if (result is int)
|
||||
{
|
||||
amount = (int)result;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryWriteFreeResource(FreeResourceType type, int amount)
|
||||
{
|
||||
if (!EnsureResourceReflection())
|
||||
return false;
|
||||
|
||||
try
|
||||
{
|
||||
object ra = freeResourceAmountField.GetValue(FreeResourceManager.inst);
|
||||
if (ra == null)
|
||||
return false;
|
||||
|
||||
resourceAmountSetMethod.Invoke(ra, new object[] { type, amount });
|
||||
if (typeof(ResourceAmount).IsValueType)
|
||||
freeResourceAmountField.SetValue(FreeResourceManager.inst, ra);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryRefreshFreeResourceUI()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!EnsureResourceReflection())
|
||||
return;
|
||||
|
||||
if (freeResourceManagerMaybeRefresh != null && FreeResourceManager.inst != null)
|
||||
freeResourceManagerMaybeRefresh.Invoke(FreeResourceManager.inst, null);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private static void TryRefreshFieldSystem()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (Player.inst == null || Player.inst.fieldSystem == null)
|
||||
return;
|
||||
|
||||
if (fieldSystemMaybeRefresh == null)
|
||||
{
|
||||
var bindingFlags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
|
||||
fieldSystemMaybeRefresh = Player.inst.fieldSystem.GetType()
|
||||
.GetMethods(bindingFlags)
|
||||
.FirstOrDefault(m =>
|
||||
m.ReturnType == typeof(void) &&
|
||||
m.GetParameters().Length == 0 &&
|
||||
(m.Name.IndexOf("Rebuild", StringComparison.OrdinalIgnoreCase) >= 0 ||
|
||||
m.Name.IndexOf("Refresh", StringComparison.OrdinalIgnoreCase) >= 0) &&
|
||||
m.Name.IndexOf("Reset", StringComparison.OrdinalIgnoreCase) < 0 &&
|
||||
m.Name.IndexOf("Clear", StringComparison.OrdinalIgnoreCase) < 0);
|
||||
}
|
||||
|
||||
if (fieldSystemMaybeRefresh != null)
|
||||
fieldSystemMaybeRefresh.Invoke(Player.inst.fieldSystem, null);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private static void ValidateAndCorrectVillagerStates()
|
||||
{
|
||||
try
|
||||
{
|
||||
int stuckVillagers = 0;
|
||||
int correctedVillagers = 0;
|
||||
|
||||
for (int i = 0; i < Villager.villagers.Count; i++)
|
||||
{
|
||||
Villager v = Villager.villagers.data[i];
|
||||
if (v == null)
|
||||
continue;
|
||||
|
||||
try
|
||||
{
|
||||
bool needsCorrection = false;
|
||||
|
||||
// Check if villager position is invalid
|
||||
if (float.IsNaN(v.Pos.x) || float.IsNaN(v.Pos.y) || float.IsNaN(v.Pos.z))
|
||||
{
|
||||
needsCorrection = true;
|
||||
stuckVillagers++;
|
||||
}
|
||||
|
||||
if (needsCorrection)
|
||||
{
|
||||
// Correct villager state
|
||||
try
|
||||
{
|
||||
// Ensure valid position
|
||||
if (float.IsNaN(v.Pos.x) || float.IsNaN(v.Pos.y) || float.IsNaN(v.Pos.z))
|
||||
{
|
||||
// Teleport to a safe position
|
||||
Vector3 safePos = new Vector3(World.inst.GridWidth / 2, 0, World.inst.GridHeight / 2);
|
||||
v.TeleportTo(safePos);
|
||||
}
|
||||
|
||||
correctedVillagers++;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Main.helper.Log($"Error correcting villager {i}: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Main.helper.Log($"Error validating villager {i}: {e.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
if (stuckVillagers > 0)
|
||||
{
|
||||
Main.helper.Log($"Villager validation: Found {stuckVillagers} stuck villagers, corrected {correctedVillagers}");
|
||||
}
|
||||
|
||||
// Force villager system refresh if we found issues
|
||||
if (stuckVillagers > 0 && VillagerSystem.inst != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
var villagerSystemType = typeof(VillagerSystem);
|
||||
var refreshMethods = villagerSystemType.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
|
||||
.Where(m => m.Name.Contains("Refresh") || m.Name.Contains("Update") || m.Name.Contains("Restart"));
|
||||
|
||||
foreach (var method in refreshMethods)
|
||||
{
|
||||
if (method.GetParameters().Length == 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
method.Invoke(VillagerSystem.inst, null);
|
||||
Main.helper.Log($"Called VillagerSystem.{method.Name} for validation");
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Main.helper.Log($"Error refreshing villager system: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Main.helper.Log("Error in villager state validation: " + e.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,11 +15,6 @@ namespace KCM.UI
|
||||
class KaC_Button
|
||||
{
|
||||
public Button Button = null;
|
||||
private static readonly string[] ButtonPaths =
|
||||
{
|
||||
"TopLevelUICanvas/TopLevel/Body/ButtonContainer/New",
|
||||
"MainMenu/TopLevel/Body/ButtonContainer/New" // fallback for older versions
|
||||
};
|
||||
|
||||
public string Name
|
||||
{
|
||||
@@ -89,18 +84,14 @@ namespace KCM.UI
|
||||
set => Transform.SetSiblingIndex(value);
|
||||
}
|
||||
|
||||
public KaC_Button(Transform parent = null) : this(null, parent) { }
|
||||
|
||||
public KaC_Button(Button b, Transform parent = null)
|
||||
public KaC_Button(Transform parent = null)
|
||||
{
|
||||
var templateButton = ResolveTemplateButton(b);
|
||||
Button b = Constants.MainMenuUI_T.Find("TopLevelUICanvas/TopLevel/Body/ButtonContainer/New").GetComponent<Button>();
|
||||
|
||||
if (templateButton == null)
|
||||
throw new InvalidOperationException("Template button not found in main menu UI.");
|
||||
|
||||
Button = parent == null
|
||||
? GameObject.Instantiate(templateButton)
|
||||
: GameObject.Instantiate(templateButton, parent);
|
||||
if (parent == null)
|
||||
Button = GameObject.Instantiate(b);
|
||||
else
|
||||
Button = GameObject.Instantiate(b, parent);
|
||||
|
||||
foreach (Localize Localize in Button.GetComponentsInChildren<Localize>())
|
||||
GameObject.Destroy(Localize);
|
||||
@@ -108,27 +99,20 @@ namespace KCM.UI
|
||||
Button.onClick = new Button.ButtonClickedEvent();
|
||||
}
|
||||
|
||||
private static Button ResolveTemplateButton(Button providedButton)
|
||||
public KaC_Button(Button b, Transform parent = null)
|
||||
{
|
||||
if (providedButton != null)
|
||||
return providedButton;
|
||||
if (b == null)
|
||||
b = Constants.MainMenuUI_T.Find("TopLevelUICanvas/TopLevel/Body/ButtonContainer/New").GetComponent<Button>();
|
||||
|
||||
foreach (var path in ButtonPaths)
|
||||
{
|
||||
var transform = Constants.MainMenuUI_T?.Find(path);
|
||||
if (transform == null)
|
||||
continue;
|
||||
if (parent == null)
|
||||
Button = GameObject.Instantiate(b);
|
||||
else
|
||||
Button = GameObject.Instantiate(b, parent);
|
||||
|
||||
var button = transform.GetComponent<Button>();
|
||||
if (button != null)
|
||||
{
|
||||
Main.helper?.Log($"Using menu button template at '{path}'.");
|
||||
return button;
|
||||
}
|
||||
}
|
||||
foreach (Localize Localize in Button.GetComponentsInChildren<Localize>())
|
||||
GameObject.Destroy(Localize);
|
||||
|
||||
Main.helper?.Log("Failed to find menu button template for KaC_Button.");
|
||||
return null;
|
||||
Button.onClick = new Button.ButtonClickedEvent();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
|
||||
125
UI/MultiplayerMenuInjector.cs
Normal file
125
UI/MultiplayerMenuInjector.cs
Normal file
@@ -0,0 +1,125 @@
|
||||
using Harmony;
|
||||
using I2.Loc;
|
||||
using Riptide.Demos.Steam.PlayerHosted;
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.UI;
|
||||
|
||||
namespace KCM.UI
|
||||
{
|
||||
[HarmonyPatch(typeof(GameState), "SetNewMode")]
|
||||
public static class MultiplayerMenuInjector
|
||||
{
|
||||
private static bool injected;
|
||||
|
||||
public static void Postfix()
|
||||
{
|
||||
try
|
||||
{
|
||||
EnsureInjected();
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Main.Log("MultiplayerMenuInjector failed");
|
||||
Main.Log(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static void EnsureInjected()
|
||||
{
|
||||
if (injected)
|
||||
return;
|
||||
|
||||
if (GameState.inst == null || GameState.inst.mainMenuMode == null)
|
||||
return;
|
||||
|
||||
GameObject mainMenuUi = GameState.inst.mainMenuMode.mainMenuUI;
|
||||
if (mainMenuUi == null)
|
||||
return;
|
||||
|
||||
Transform buttonContainer = mainMenuUi.transform.Find("TopLevelUICanvas/TopLevel/Body/ButtonContainer");
|
||||
if (buttonContainer == null)
|
||||
return;
|
||||
|
||||
if (buttonContainer.Find("KCM_Multiplayer") != null)
|
||||
{
|
||||
injected = true;
|
||||
return;
|
||||
}
|
||||
|
||||
Button template = mainMenuUi.transform.Find("TopLevelUICanvas/TopLevel/Body/ButtonContainer/New")?.GetComponent<Button>();
|
||||
if (template == null)
|
||||
return;
|
||||
|
||||
Button button = Object.Instantiate(template, buttonContainer);
|
||||
button.name = "KCM_Multiplayer";
|
||||
|
||||
foreach (Localize loc in button.GetComponentsInChildren<Localize>())
|
||||
Object.Destroy(loc);
|
||||
|
||||
button.onClick = new Button.ButtonClickedEvent();
|
||||
|
||||
TextMeshProUGUI label = button.GetComponentInChildren<TextMeshProUGUI>();
|
||||
if (label != null)
|
||||
label.text = "Multiplayer";
|
||||
|
||||
int insertIndex = template.transform.GetSiblingIndex() + 1;
|
||||
button.transform.SetSiblingIndex(insertIndex);
|
||||
|
||||
button.onClick.AddListener(OpenServerBrowser);
|
||||
|
||||
injected = true;
|
||||
Main.Log("Injected Multiplayer menu button");
|
||||
}
|
||||
|
||||
private static void OpenServerBrowser()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Ensure Steam + lobby manager exist
|
||||
_ = KCMSteamManager.Instance;
|
||||
|
||||
LobbyManager lobbyManager = Object.FindObjectOfType<LobbyManager>();
|
||||
if (lobbyManager == null)
|
||||
{
|
||||
GameObject go = new GameObject("KCM_LobbyManager");
|
||||
Object.DontDestroyOnLoad(go);
|
||||
lobbyManager = go.AddComponent<LobbyManager>();
|
||||
}
|
||||
|
||||
// Ensure prefabs loaded
|
||||
if (PrefabManager.serverBrowserPrefab == null || PrefabManager.serverLobbyPrefab == null)
|
||||
{
|
||||
if (Main.helper != null)
|
||||
new PrefabManager().PreScriptLoad(Main.helper);
|
||||
}
|
||||
|
||||
// Ensure UI initialized
|
||||
KCM.ServerBrowser browser = Object.FindObjectOfType<KCM.ServerBrowser>();
|
||||
if (browser == null)
|
||||
{
|
||||
GameObject go = new GameObject("KCM_ServerBrowser");
|
||||
Object.DontDestroyOnLoad(go);
|
||||
browser = go.AddComponent<KCM.ServerBrowser>();
|
||||
}
|
||||
|
||||
browser.EnsureUi();
|
||||
|
||||
if (KCM.ServerBrowser.serverLobbyRef != null)
|
||||
KCM.ServerBrowser.serverLobbyRef.SetActive(false);
|
||||
|
||||
if (KCM.ServerBrowser.serverBrowserRef != null)
|
||||
KCM.ServerBrowser.serverBrowserRef.SetActive(true);
|
||||
|
||||
if (GameState.inst != null && GameState.inst.mainMenuMode != null)
|
||||
GameState.inst.mainMenuMode.mainMenuUI.SetActive(false);
|
||||
}
|
||||
catch (System.Exception ex)
|
||||
{
|
||||
Main.Log("Failed opening server browser");
|
||||
Main.Log(ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user