Merge branch 'codex'
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -23,4 +23,5 @@ Desktop.ini
|
||||
**/*.mdb
|
||||
**/*.pdb
|
||||
|
||||
/.claude
|
||||
/.claude
|
||||
/*.png
|
||||
@@ -1,10 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace KCM.Attributes
|
||||
{
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
|
||||
public class NoServerRelayAttribute : Attribute
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
104
CLAUDE.md
Normal file
104
CLAUDE.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a **Kingdoms and Castles Multiplayer Mod** that adds multiplayer functionality to the game using:
|
||||
- **Riptide Networking** library for low-level networking
|
||||
- **Steam P2P** transport for NAT traversal
|
||||
- **Harmony** for non-invasive game modification via patches/hooks
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `Main.cs` | Entry point, Harmony patches, all game event hooks |
|
||||
| `KCClient.cs` | Client-side networking wrapper around Riptide.Client |
|
||||
| `KCServer.cs` | Server-side networking, client management |
|
||||
| `KCPlayer.cs` | Player data container (id, steamId, inst, kingdomName) |
|
||||
|
||||
### Networking Layer
|
||||
|
||||
```
|
||||
Riptide.Client/Server
|
||||
└── SteamClient/SteamServer (Steam P2P transport)
|
||||
└── KCClient/KCServer wrappers
|
||||
└── PacketHandler (serialization/routing)
|
||||
```
|
||||
|
||||
- Port: 7777, Max clients: 25
|
||||
- Team ID formula: `clientId * 10 + 2`
|
||||
|
||||
### Packet System
|
||||
|
||||
Located in `/Packets/`:
|
||||
- Base class: `Packet.cs` with `Send()`, `SendToAll()`, `HandlePacketClient()`, `HandlePacketServer()`
|
||||
- `PacketHandler.cs` uses reflection for automatic serialization based on property names (alphabetical order)
|
||||
- Packet IDs defined in `Enums/Packets.cs`
|
||||
|
||||
Key packet ranges:
|
||||
- 25-34: Lobby (chat, player list, settings)
|
||||
- 70-79: World/building updates
|
||||
- 85: Save transfer (chunked)
|
||||
- 87-90: Building state, villagers
|
||||
|
||||
### State Synchronization
|
||||
|
||||
- **Buildings**: Observer pattern in `StateManagement/BuildingState/` - monitors field changes every 100ms, sends updates every 300ms
|
||||
- **Villagers**: Event-based sync via Harmony hooks on `VillagerSystem.AddVillager`, `Villager.TeleportTo`
|
||||
- **Save/Load**: Custom `MultiplayerSaveContainer` extends `LoadSaveContainer`, stores per-player data
|
||||
|
||||
### Harmony Hooks Pattern
|
||||
|
||||
All hooks check call stack to prevent infinite loops:
|
||||
```csharp
|
||||
if (new StackFrame(3).GetMethod().Name.Contains("HandlePacket"))
|
||||
return; // Skip if called by network handler
|
||||
```
|
||||
|
||||
### Key Dictionaries
|
||||
|
||||
```csharp
|
||||
Main.kCPlayers // Dictionary<steamId, KCPlayer>
|
||||
Main.clientSteamIds // Dictionary<clientId, steamId>
|
||||
```
|
||||
|
||||
## Common Issues & Patterns
|
||||
|
||||
### Player Resolution
|
||||
```csharp
|
||||
Main.GetPlayerByClientID(clientId) // clientId -> KCPlayer
|
||||
Main.GetPlayerByTeamID(teamId) // teamId -> Player.inst
|
||||
Main.GetPlayerByBuilding(building) // building -> owner Player
|
||||
```
|
||||
|
||||
### Building Ownership
|
||||
Buildings are associated with players via `LandmassOwner.teamId`. Use `building.TeamID()` to determine owner.
|
||||
|
||||
### Save Directory
|
||||
Multiplayer saves go to: `Application.persistentDataPath + "/Saves/Multiplayer"`
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
/Attributes - Custom packet attributes
|
||||
/Enums - Packet types, menu states
|
||||
/LoadSaveOverrides - MultiplayerSaveContainer
|
||||
/Packets - All network packets
|
||||
/Riptide - Networking library
|
||||
/RiptideSteamTransport - Steam P2P adapter, LobbyManager
|
||||
/StateManagement - Observer pattern for sync
|
||||
/ServerLobby - Lobby UI
|
||||
/ServerBrowser - Server discovery
|
||||
/UI - Custom UI elements
|
||||
```
|
||||
|
||||
## Known Architecture Limitations
|
||||
|
||||
1. Static `Client`/`Server` instances can cause issues on reconnect
|
||||
2. Call stack checking for loop prevention is fragile
|
||||
3. No conflict resolution - last-write-wins
|
||||
4. Villager sync is event-based only, no continuous state updates
|
||||
@@ -44,10 +44,6 @@ namespace KCM.Enums
|
||||
AddVillager = 88,
|
||||
SetupInitialWorkers = 89,
|
||||
VillagerTeleportTo = 90,
|
||||
PlaceKeepRandomly = 91,
|
||||
ResyncRequest = 92,
|
||||
ResourceSnapshot = 93,
|
||||
BuildingSnapshot = 94,
|
||||
VillagerSnapshot = 95
|
||||
PlaceKeepRandomly = 91
|
||||
}
|
||||
}
|
||||
|
||||
17
KCClient.cs
17
KCClient.cs
@@ -39,8 +39,6 @@ namespace KCM
|
||||
Main.helper.Log("Client disconnected event start");
|
||||
try
|
||||
{
|
||||
Main.ResetMultiplayerState("Client disconnected");
|
||||
|
||||
if (e.Message != null)
|
||||
{
|
||||
Main.helper.Log(e.Message.ToString());
|
||||
@@ -78,19 +76,7 @@ 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());
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -102,7 +88,6 @@ namespace KCM
|
||||
public static void Connect(string ip)
|
||||
{
|
||||
Main.helper.Log("Trying to connect to: " + ip);
|
||||
try { Application.runInBackground = true; } catch { }
|
||||
client.Connect(ip, useMessageHandlers: false);
|
||||
}
|
||||
|
||||
|
||||
134
KCServer.cs
134
KCServer.cs
@@ -18,21 +18,22 @@ namespace KCM
|
||||
{
|
||||
public class KCServer : MonoBehaviour
|
||||
{
|
||||
public static Server server = new Server(Main.steamServer);
|
||||
public static Server server = null;
|
||||
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"));
|
||||
|
||||
server.MessageReceived += PacketHandler.HandlePacketServer;
|
||||
}
|
||||
|
||||
public static void StartServer()
|
||||
{
|
||||
// Stop and cleanup existing server if running
|
||||
if (server != null)
|
||||
{
|
||||
if (server.IsRunning)
|
||||
{
|
||||
server.Stop();
|
||||
}
|
||||
// Unsubscribe old event handlers to prevent memory leaks
|
||||
server.MessageReceived -= PacketHandler.HandlePacketServer;
|
||||
}
|
||||
|
||||
server = new Server(Main.steamServer);
|
||||
server.MessageReceived += PacketHandler.HandlePacketServer;
|
||||
|
||||
@@ -53,7 +54,6 @@ namespace KCM
|
||||
}
|
||||
|
||||
ev.Client.CanQualityDisconnect = false;
|
||||
ev.Client.MaxSendAttempts = 50;
|
||||
|
||||
Main.helper.Log("Client ID is: " + ev.Client.Id);
|
||||
|
||||
@@ -62,42 +62,15 @@ namespace KCM
|
||||
|
||||
server.ClientDisconnected += (obj, ev) =>
|
||||
{
|
||||
try
|
||||
new ChatSystemMessage()
|
||||
{
|
||||
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;
|
||||
Message = $"{Main.GetPlayerByClientID(ev.Client.Id).name} has left the server.",
|
||||
}.SendToAll();
|
||||
|
||||
Main.kCPlayers.Remove(steamId);
|
||||
}
|
||||
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.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($"Client disconnected. {ev.Reason}");
|
||||
};
|
||||
|
||||
Main.helper.Log($"Listening on port 7777. Max {LobbyHandler.ServerSettings.MaxPlayers} clients.");
|
||||
@@ -126,79 +99,18 @@ namespace KCM
|
||||
}
|
||||
}*/
|
||||
|
||||
public static bool IsRunning { get { return server.IsRunning; } }
|
||||
public static bool IsRunning { get { return server != null && server.IsRunning; } }
|
||||
|
||||
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}");
|
||||
if (server != null)
|
||||
server.Update();
|
||||
}
|
||||
|
||||
private void OnApplicationQuit()
|
||||
{
|
||||
server.Stop();
|
||||
if (server != null && server.IsRunning)
|
||||
server.Stop();
|
||||
}
|
||||
|
||||
private void Preload(KCModHelper helper)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Assets.Code;
|
||||
using Assets.Code;
|
||||
using Riptide;
|
||||
using Riptide.Transports;
|
||||
using Steamworks;
|
||||
@@ -8,7 +8,6 @@ using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace KCM.LoadSaveOverrides
|
||||
{
|
||||
@@ -27,39 +26,12 @@ namespace KCM.LoadSaveOverrides
|
||||
|
||||
foreach (var player in Main.kCPlayers.Values)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (player == null)
|
||||
continue;
|
||||
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);
|
||||
|
||||
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());
|
||||
}
|
||||
Main.helper.Log($"{players[player.steamId] == null}");
|
||||
}
|
||||
|
||||
this.WorldSaveData = new World.WorldSaveData().Pack(World.inst);
|
||||
@@ -71,25 +43,7 @@ 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);
|
||||
|
||||
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.ShipSystemSaveData = new ShipSystem.ShipSystemSaveData().Pack(ShipSystem.inst);
|
||||
this.AIBrainsSaveData = new AIBrainsContainer.SaveData().Pack(AIBrainsContainer.inst);
|
||||
this.SiegeMonsterSaveData = new SiegeMonster.SiegeMonsterSaveData().Pack(null);
|
||||
this.CartSystemSaveData = new CartSystem.CartSystemSaveData().Pack(CartSystem.inst);
|
||||
@@ -104,6 +58,7 @@ namespace KCM.LoadSaveOverrides
|
||||
{
|
||||
foreach (var kvp in players)
|
||||
{
|
||||
|
||||
KCPlayer player;
|
||||
|
||||
if (!Main.kCPlayers.TryGetValue(kvp.Key, out player))
|
||||
@@ -118,6 +73,7 @@ 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);
|
||||
@@ -129,6 +85,9 @@ namespace KCM.LoadSaveOverrides
|
||||
}
|
||||
this.TownNameSaveData.Unpack(TownNameUI.inst);
|
||||
|
||||
|
||||
TownNameUI.inst.SetTownName(kingdomNames[Main.PlayerSteamID]);
|
||||
|
||||
Main.helper.Log("Unpacking player data");
|
||||
|
||||
Player.PlayerSaveData clientPlayerData = null;
|
||||
@@ -142,9 +101,10 @@ 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))
|
||||
@@ -157,63 +117,39 @@ 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);
|
||||
clientPlayerData.Unpack(Player.inst); // Unpack the current client player last so that loading of villagers works correctly.
|
||||
|
||||
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");
|
||||
|
||||
Main.helper.Log("Unpacking AI brains");
|
||||
bool flag10 = this.AIBrainsSaveData != null;
|
||||
if (flag10)
|
||||
/*
|
||||
* Not even going to bother fixing AI brains save data yet, not in short-term roadmap
|
||||
*/
|
||||
|
||||
/*bool flag2 = this.AIBrainsSaveData != null;
|
||||
if (flag2)
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
this.AIBrainsSaveData.UnpackPrePlayer(AIBrainsContainer.inst);
|
||||
}*/
|
||||
|
||||
Main.helper.Log("Unpacking free resource manager");
|
||||
this.FreeResourceManagerSaveData.Unpack(FreeResourceManager.inst);
|
||||
@@ -268,6 +204,7 @@ 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);
|
||||
@@ -280,19 +217,6 @@ 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);
|
||||
@@ -301,38 +225,75 @@ 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, 3);
|
||||
loadTickDelayField.SetValue(Player.inst, 1);
|
||||
}
|
||||
|
||||
// UnitSystem.inst.loadTickDelay = 1;
|
||||
Type unitSystemType = typeof(UnitSystem);
|
||||
loadTickDelayField = unitSystemType.GetField("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
if (loadTickDelayField != null)
|
||||
{
|
||||
loadTickDelayField.SetValue(UnitSystem.inst, 3);
|
||||
loadTickDelayField.SetValue(UnitSystem.inst, 1);
|
||||
}
|
||||
|
||||
// JobSystem.inst.loadTickDelay = 1;
|
||||
Type jobSystemType = typeof(JobSystem);
|
||||
loadTickDelayField = jobSystemType.GetField("loadTickDelay", BindingFlags.Instance | BindingFlags.NonPublic);
|
||||
if (loadTickDelayField != null)
|
||||
{
|
||||
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);
|
||||
loadTickDelayField.SetValue(JobSystem.inst, 1);
|
||||
}
|
||||
|
||||
Main.helper.Log($"Setting kingdom name to: {kingdomNames[Main.PlayerSteamID]}");
|
||||
TownNameUI.inst.SetTownName(kingdomNames[Main.PlayerSteamID]);
|
||||
|
||||
// Post-load fixes for multiplayer
|
||||
Main.helper.Log("Running post-load multiplayer fixes...");
|
||||
|
||||
// Fix 1: Re-register all resource storages for all players
|
||||
Main.helper.Log("Re-registering resource storages...");
|
||||
foreach (var kcPlayer in Main.kCPlayers.Values)
|
||||
{
|
||||
if (kcPlayer.inst == null) continue;
|
||||
|
||||
foreach (var building in kcPlayer.inst.Buildings.data)
|
||||
{
|
||||
if (building == null) continue;
|
||||
|
||||
// Re-register resource storages
|
||||
var storages = KCM.ResourceStorageHelper.GetStorages(building);
|
||||
foreach (var storage in storages)
|
||||
{
|
||||
if (storage != null && !KCM.ResourceStorageHelper.IsPrivate(storage))
|
||||
{
|
||||
try
|
||||
{
|
||||
KCM.ResourceStorageHelper.Register(storage);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Main.helper.Log($"Error re-registering storage for {building.UniqueName}: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh building pathing
|
||||
try
|
||||
{
|
||||
building.BakePathing();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
Main.helper.Log("Post-load multiplayer fixes complete.");
|
||||
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace KCM.Packets.Game.GamePlayer
|
||||
public override ushort packetId => (ushort)Enums.Packets.AddVillager;
|
||||
|
||||
public Guid guid { get; set; }
|
||||
public Vector3 position { get; set; }
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
@@ -19,18 +20,33 @@ namespace KCM.Packets.Game.GamePlayer
|
||||
{
|
||||
if (KCClient.client.Id == clientId) return;
|
||||
|
||||
// Check for duplicate villager by guid
|
||||
var existingVillager = player.inst.Workers.data.FirstOrDefault(w => w != null && w.guid == guid);
|
||||
if (existingVillager != null)
|
||||
{
|
||||
Main.helper.Log($"Villager with guid {guid} already exists, skipping duplicate");
|
||||
return;
|
||||
}
|
||||
|
||||
Main.helper.Log("Received add villager packet from " + player.name + $"({player.id})");
|
||||
|
||||
Villager v = Villager.CreateVillager();
|
||||
v.guid = guid;
|
||||
Villager newVillager = Villager.CreateVillager();
|
||||
newVillager.guid = guid;
|
||||
|
||||
player.inst.Workers.Add(v);
|
||||
player.inst.Homeless.Add(v);
|
||||
// Set villager position
|
||||
if (position != Vector3.zero)
|
||||
{
|
||||
newVillager.TeleportTo(position);
|
||||
}
|
||||
|
||||
player.inst.Workers.Add(newVillager);
|
||||
player.inst.Homeless.Add(newVillager);
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Main.helper.Log("Error handling add villager packet: " + e.Message);
|
||||
Main.helper.Log(e.StackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,56 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEngine;
|
||||
|
||||
namespace KCM.Packets.Game.GameVillager
|
||||
{
|
||||
public class VillagerSnapshotPacket : Packet
|
||||
{
|
||||
public override ushort packetId => (ushort)Enums.Packets.VillagerSnapshot;
|
||||
|
||||
public List<Guid> guids { get; set; } = new List<Guid>();
|
||||
public List<Vector3> positions { get; set; } = new List<Vector3>();
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
if (KCClient.client != null && clientId == KCClient.client.Id)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
int count = Math.Min(guids?.Count ?? 0, positions?.Count ?? 0);
|
||||
if (count == 0)
|
||||
return;
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Guid guid = guids[i];
|
||||
Vector3 position = positions[i];
|
||||
|
||||
Villager villager = null;
|
||||
try
|
||||
{
|
||||
villager = Villager.villagers?.data.FirstOrDefault(v => v != null && v.guid == guid);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
if (villager == null)
|
||||
continue;
|
||||
|
||||
villager.TeleportTo(position);
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Main.helper.Log("Error handling villager snapshot packet: " + e.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,14 @@ namespace KCM.Packets.Game.GameWorld
|
||||
{
|
||||
Main.helper.Log("Received place building packet for " + uniqueName + " from " + player.name + $"({player.id})");
|
||||
|
||||
// Check for duplicate building by guid to prevent double placement from network retries
|
||||
var existingBuilding = player.inst.Buildings.data.FirstOrDefault(b => b != null && b.guid == guid);
|
||||
if (existingBuilding != null)
|
||||
{
|
||||
Main.helper.Log($"Building with guid {guid} already exists for player {player.name}, skipping duplicate placement");
|
||||
return;
|
||||
}
|
||||
|
||||
//var originalPlayer = Player.inst;
|
||||
//Player.inst = player.inst;
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
|
||||
namespace KCM.Packets.Game
|
||||
{
|
||||
@@ -12,30 +11,18 @@ 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;
|
||||
|
||||
try
|
||||
{
|
||||
Main.helper.Log($"Received SetSpeed packet: speed={speed}, isPaused={isPaused}");
|
||||
|
||||
// Simply apply the speed - SpeedControlUISetSpeedHook will handle this correctly
|
||||
// since we're coming from a packet (PacketHandler.IsHandlingPacket will be true)
|
||||
SpeedControlUI.inst.SetSpeed(speed);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Main.helper.Log("Error handling SetSpeed packet: " + e.Message);
|
||||
}
|
||||
SpeedControlUI.inst.SetSpeed(speed);
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
// Server relay is handled automatically by PacketHandler unless [NoServerRelay] is used.
|
||||
//throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,6 @@ namespace KCM.Packets.Handlers
|
||||
if (!KCServer.IsRunning)
|
||||
{
|
||||
Main.kCPlayers.Clear();
|
||||
Main.clientSteamIds.Clear();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -15,14 +15,6 @@ namespace KCM.Packets.Handlers
|
||||
{
|
||||
public class PacketHandler
|
||||
{
|
||||
[ThreadStatic]
|
||||
private static bool isHandlingPacket;
|
||||
|
||||
public static bool IsHandlingPacket
|
||||
{
|
||||
get { return isHandlingPacket; }
|
||||
}
|
||||
|
||||
public static Dictionary<ushort, PacketRef> Packets = new Dictionary<ushort, PacketRef>();
|
||||
public class PacketRef
|
||||
{
|
||||
@@ -92,9 +84,7 @@ namespace KCM.Packets.Handlers
|
||||
{
|
||||
|
||||
IPacket p = (IPacket)Activator.CreateInstance(packet);
|
||||
var properties = packet.GetProperties(BindingFlags.Instance | BindingFlags.Public)
|
||||
.Where(prop => prop.Name != "packetId" && prop.Name != "sendMode")
|
||||
.ToArray();
|
||||
var properties = packet.GetProperties(BindingFlags.Instance | BindingFlags.Public).Where(prop => prop.Name != "packetId").ToArray();
|
||||
Array.Sort(properties, (x, y) => String.Compare(x.Name, y.Name));
|
||||
ushort id = (ushort)p.GetType().GetProperty("packetId").GetValue(p, null);
|
||||
|
||||
@@ -147,9 +137,7 @@ namespace KCM.Packets.Handlers
|
||||
{
|
||||
packet.HandlePacketServer();
|
||||
|
||||
bool shouldRelay = packet.GetType().GetCustomAttributes(typeof(NoServerRelayAttribute), inherit: true).Length == 0;
|
||||
if (shouldRelay)
|
||||
((Packet)packet).SendToAll();
|
||||
((Packet)packet).SendToAll();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -191,7 +179,6 @@ namespace KCM.Packets.Handlers
|
||||
{
|
||||
try
|
||||
{
|
||||
isHandlingPacket = true;
|
||||
packet.HandlePacketClient();
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -214,10 +201,6 @@ namespace KCM.Packets.Handlers
|
||||
Main.helper.Log(ex.InnerException.StackTrace);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
isHandlingPacket = false;
|
||||
}
|
||||
}
|
||||
|
||||
/* if (PacketHandlers.TryGetValue(id, out PacketHandlerDelegate handler))
|
||||
@@ -239,20 +222,14 @@ namespace KCM.Packets.Handlers
|
||||
try
|
||||
{
|
||||
var packetRef = Packets[packet.packetId];
|
||||
|
||||
MessageSendMode sendMode = MessageSendMode.Reliable;
|
||||
Packet basePacket = packet as Packet;
|
||||
if (basePacket != null)
|
||||
sendMode = basePacket.sendMode;
|
||||
|
||||
Message message = Message.Create(sendMode, packet.packetId);
|
||||
Message message = Message.Create(MessageSendMode.Reliable, packet.packetId);
|
||||
|
||||
foreach (var prop in packetRef.properties)
|
||||
{
|
||||
if (prop.PropertyType.IsEnum)
|
||||
{
|
||||
currentPropName = prop.Name;
|
||||
message.AddInt(Convert.ToInt32(prop.GetValue(packet, null)));
|
||||
message.AddInt((int)prop.GetValue(packet, null));
|
||||
}
|
||||
else if (prop.PropertyType == typeof(ushort))
|
||||
{
|
||||
@@ -323,27 +300,6 @@ namespace KCM.Packets.Handlers
|
||||
message.AddInt(item);
|
||||
}
|
||||
|
||||
else if (prop.PropertyType == typeof(List<Guid>))
|
||||
{
|
||||
currentPropName = prop.Name;
|
||||
List<Guid> list = (List<Guid>)prop.GetValue(packet, null);
|
||||
message.AddInt(list.Count);
|
||||
foreach (var item in list)
|
||||
message.AddBytes(item.ToByteArray(), true);
|
||||
}
|
||||
else if (prop.PropertyType == typeof(List<Vector3>))
|
||||
{
|
||||
currentPropName = prop.Name;
|
||||
List<Vector3> list = (List<Vector3>)prop.GetValue(packet, null);
|
||||
message.AddInt(list.Count);
|
||||
foreach (var item in list)
|
||||
{
|
||||
message.AddFloat(item.x);
|
||||
message.AddFloat(item.y);
|
||||
message.AddFloat(item.z);
|
||||
}
|
||||
}
|
||||
|
||||
else if (prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
|
||||
{
|
||||
currentPropName = prop.Name;
|
||||
@@ -505,7 +461,9 @@ namespace KCM.Packets.Handlers
|
||||
if (prop.PropertyType.IsEnum)
|
||||
{
|
||||
int enumValue = message.GetInt();
|
||||
prop.SetValue(p, Enum.ToObject(prop.PropertyType, enumValue));
|
||||
string enumName = Enum.GetName(prop.PropertyType, enumValue);
|
||||
|
||||
prop.SetValue(p, Enum.Parse(prop.PropertyType, enumName));
|
||||
}
|
||||
else if (prop.PropertyType == typeof(ushort))
|
||||
{
|
||||
@@ -577,29 +535,6 @@ namespace KCM.Packets.Handlers
|
||||
|
||||
prop.SetValue(p, list);
|
||||
}
|
||||
else if (prop.PropertyType == typeof(List<Guid>))
|
||||
{
|
||||
int count = message.GetInt();
|
||||
List<Guid> list = new List<Guid>();
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
list.Add(new Guid(message.GetBytes()));
|
||||
|
||||
prop.SetValue(p, list);
|
||||
}
|
||||
else if (prop.PropertyType == typeof(List<Vector3>))
|
||||
{
|
||||
int count = message.GetInt();
|
||||
List<Vector3> list = new List<Vector3>();
|
||||
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
Vector3 vector = new Vector3(message.GetFloat(), message.GetFloat(), message.GetFloat());
|
||||
list.Add(vector);
|
||||
}
|
||||
|
||||
prop.SetValue(p, list);
|
||||
}
|
||||
else if (prop.PropertyType.IsGenericType && prop.PropertyType.GetGenericTypeDefinition() == typeof(Dictionary<,>))
|
||||
{
|
||||
IDictionary dictionary = (IDictionary)prop.GetValue(p, null);
|
||||
|
||||
@@ -35,17 +35,13 @@ namespace KCM.Packets.Lobby
|
||||
|
||||
Main.helper.Log("PlayerList: " + playersName[i] + " " + playersId[i] + " " + steamIds[i]);
|
||||
|
||||
KCPlayer player;
|
||||
if (!Main.kCPlayers.TryGetValue(steamIds[i], out player) || player == null)
|
||||
Main.kCPlayers.Add(steamIds[i], new KCPlayer(playersName[i], playersId[i], steamIds[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];
|
||||
name = playersName[i],
|
||||
ready = playersReady[i],
|
||||
banner = playersBanner[i],
|
||||
kingdomName = playersKingdomName[i]
|
||||
});
|
||||
|
||||
|
||||
if (Main.clientSteamIds.ContainsKey(playersId[i]))
|
||||
@@ -53,8 +49,7 @@ namespace KCM.Packets.Lobby
|
||||
else
|
||||
Main.clientSteamIds.Add(playersId[i], steamIds[i]);
|
||||
|
||||
if (player.inst != null && player.inst.PlayerLandmassOwner != null)
|
||||
player.inst.PlayerLandmassOwner.SetBannerIdx(playersBanner[i]);
|
||||
Main.kCPlayers[steamIds[i]].inst.PlayerLandmassOwner.SetBannerIdx(playersBanner[i]);
|
||||
|
||||
LobbyHandler.AddPlayerEntry(playersId[i]);
|
||||
}
|
||||
|
||||
@@ -14,8 +14,6 @@ namespace KCM.Packets.Lobby
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
if (player == null)
|
||||
return;
|
||||
IsReady = !player.ready;
|
||||
//SendToAll(KCClient.client.Id);
|
||||
|
||||
@@ -24,8 +22,6 @@ namespace KCM.Packets.Lobby
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
if (player == null)
|
||||
return;
|
||||
player.ready = IsReady;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using KCM.StateManagement.Observers;
|
||||
using static KCM.Main;
|
||||
|
||||
namespace KCM.Packets.Lobby
|
||||
@@ -18,14 +17,6 @@ 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; }
|
||||
@@ -38,56 +29,38 @@ namespace KCM.Packets.Lobby
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
bool initialisingTransfer = !loadingSave ||
|
||||
saveData == null ||
|
||||
saveData.Length != saveSize ||
|
||||
chunksReceived == null ||
|
||||
chunksReceived.Length != totalChunks;
|
||||
float savePercent = (float)received / (float)saveSize;
|
||||
|
||||
if (initialisingTransfer)
|
||||
// Initialize saveData and chunksReceived on the first packet received
|
||||
if (saveData.Length == 1)
|
||||
{
|
||||
|
||||
Main.helper.Log("Save Transfer started!");
|
||||
loadingSave = true;
|
||||
received = 0;
|
||||
|
||||
StateObserver.ClearAll();
|
||||
ServerLobbyScript.LoadingSave.SetActive(true);
|
||||
|
||||
// save percentage
|
||||
|
||||
|
||||
saveData = new byte[saveSize];
|
||||
chunksReceived = new bool[totalChunks];
|
||||
|
||||
if (ServerLobbyScript.LoadingSave != null)
|
||||
ServerLobbyScript.LoadingSave.SetActive(true);
|
||||
}
|
||||
|
||||
if (chunkId < 0 || chunkId >= totalChunks)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
// Copy the chunk data into the correct position in saveData
|
||||
Array.Copy(saveDataChunk, 0, saveData, saveDataIndex, saveDataChunk.Length);
|
||||
|
||||
// Mark this chunk as received
|
||||
chunksReceived[chunkId] = true;
|
||||
|
||||
// Seek to the next position to write to
|
||||
received += chunkSize;
|
||||
|
||||
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") + "%";
|
||||
if (ServerLobbyScript.ProgressText != null)
|
||||
ServerLobbyScript.ProgressText.text = $"{((float)(received / 1000)).ToString("0.00")} KB / {((float)(saveSize / 1000)).ToString("0.00")} KB";
|
||||
|
||||
ServerLobbyScript.ProgressBar.fillAmount = savePercent;
|
||||
ServerLobbyScript.ProgressBarText.text = (savePercent * 100).ToString("0.00") + "%";
|
||||
ServerLobbyScript.ProgressText.text = $"{((float)(received / 1000)).ToString("0.00")} KB / {((float)(saveSize / 1000)).ToString("0.00")} KB";
|
||||
|
||||
|
||||
if (chunkId + 1 == totalChunks)
|
||||
@@ -108,40 +81,11 @@ namespace KCM.Packets.Lobby
|
||||
|
||||
LoadSave.Load();
|
||||
|
||||
try
|
||||
{
|
||||
Main.SetMultiplayerSaveLoadInProgress(true);
|
||||
LoadSaveLoadHook.saveContainer.Unpack(null);
|
||||
}
|
||||
finally
|
||||
{
|
||||
Main.SetMultiplayerSaveLoadInProgress(false);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
RunPostLoadRebuild("Save transfer complete");
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
LoadSaveLoadHook.saveContainer.Unpack(null);
|
||||
Broadcast.OnLoadedEvent.Broadcast(new OnLoadedEvent());
|
||||
|
||||
try
|
||||
{
|
||||
new KCM.Packets.Network.ResyncRequestPacket { reason = "post-load" }.Send();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
if (ServerLobbyScript.LoadingSave != null)
|
||||
ServerLobbyScript.LoadingSave.SetActive(false);
|
||||
|
||||
loadingSave = false;
|
||||
received = 0;
|
||||
saveData = new byte[1];
|
||||
chunksReceived = new bool[1];
|
||||
ServerLobbyScript.LoadingSave.SetActive(false);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,6 @@ namespace KCM.Packets.Lobby
|
||||
|
||||
try
|
||||
{
|
||||
int desiredSpeed = 1;
|
||||
if (!LobbyManager.loadingSave)
|
||||
{
|
||||
SpeedControlUI.inst.SetSpeed(0);
|
||||
@@ -40,13 +39,12 @@ namespace KCM.Packets.Lobby
|
||||
Main.helper.Log(ex.ToString());
|
||||
}
|
||||
|
||||
SpeedControlUI.inst.SetSpeed(desiredSpeed);
|
||||
SpeedControlUI.inst.SetSpeed(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
LobbyManager.loadingSave = false;
|
||||
GameState.inst.SetNewMode(GameState.inst.playingMode);
|
||||
SpeedControlUI.inst.SetSpeed(desiredSpeed);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -33,14 +33,6 @@ 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,20 +51,6 @@ 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)
|
||||
@@ -92,7 +78,36 @@ namespace KCM.Packets.Network
|
||||
return;
|
||||
|
||||
byte[] bytes = LoadSaveLoadAtPathHook.saveData;
|
||||
KCServer.EnqueueSaveTransfer(clientId, bytes);
|
||||
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
|
||||
}.Send(clientId);
|
||||
|
||||
Main.helper.Log(" ");
|
||||
|
||||
packetsSent++;
|
||||
sent += chunk.Length;
|
||||
}
|
||||
|
||||
Main.helper.Log($"Sent {packetsSent} save data chunks to client");
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,13 +33,21 @@ namespace KCM.Packets.Network
|
||||
CloudSystem.inst.BaseFreq = 4.5f;
|
||||
Weather.inst.SetSeason(Weather.Season.Summer);
|
||||
|
||||
//inst = new KCClient(KCServer.IsRunning ? "Ryan" : "Orion");
|
||||
KCClient.inst = new KCClient(SteamFriends.GetPersonaName());
|
||||
|
||||
Main.helper.Log("Sending client connected. Client ID is: " + clientId);
|
||||
|
||||
Main.kCPlayers[Main.PlayerSteamID] = new KCPlayer(KCClient.inst.Name, clientId, Main.PlayerSteamID);
|
||||
Main.clientSteamIds[clientId] = Main.PlayerSteamID;
|
||||
KCPlayer localPlayer;
|
||||
if (!Main.kCPlayers.TryGetValue(Main.PlayerSteamID, out localPlayer))
|
||||
{
|
||||
localPlayer = new KCPlayer(KCClient.inst.Name, clientId, Main.PlayerSteamID);
|
||||
Main.kCPlayers.Add(Main.PlayerSteamID, localPlayer);
|
||||
}
|
||||
else
|
||||
{
|
||||
localPlayer.id = clientId;
|
||||
localPlayer.name = KCClient.inst.Name;
|
||||
}
|
||||
|
||||
Player.inst.PlayerLandmassOwner.teamId = clientId * 10 + 2;
|
||||
|
||||
|
||||
@@ -11,21 +11,25 @@ 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
|
||||
{
|
||||
string steamId;
|
||||
if (!Main.clientSteamIds.TryGetValue(clientId, out steamId) || string.IsNullOrEmpty(steamId))
|
||||
KCPlayer p = null;
|
||||
|
||||
if (!Main.clientSteamIds.ContainsKey(clientId))
|
||||
return null;
|
||||
|
||||
KCPlayer player;
|
||||
if (Main.kCPlayers.TryGetValue(steamId, out player))
|
||||
return player;
|
||||
//Main.helper.Log($"SteamID: {Main.GetPlayerByClientID(clientId).steamId} for {clientId} ({Main.GetPlayerByClientID(clientId).id})");
|
||||
|
||||
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}");
|
||||
}
|
||||
|
||||
Main.helper.Log($"Error getting player from packet {packetId} {GetType().Name} from {clientId}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
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,7 +11,6 @@ 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; }
|
||||
|
||||
@@ -1,37 +0,0 @@
|
||||
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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
75
README.md
75
README.md
@@ -1,75 +0,0 @@
|
||||
# KCM (Kingdoms and Castles Multiplayer)
|
||||
|
||||
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.
|
||||
|
||||
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).
|
||||
|
||||
## Mit szinkronizál a mod? (jelenlegi állapot)
|
||||
|
||||
**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.
|
||||
|
||||
**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.
|
||||
|
||||
**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).
|
||||
|
||||
**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.
|
||||
|
||||
## Mi nincs (még) rendesen szinkronizálva? (gyakori desync okok)
|
||||
|
||||
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.
|
||||
|
||||
## Mit érdemes még hozzáadni? (roadmap)
|
||||
|
||||
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).
|
||||
|
||||
71
ResourceStorageHelper.cs
Normal file
71
ResourceStorageHelper.cs
Normal file
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
|
||||
namespace KCM
|
||||
{
|
||||
internal static class ResourceStorageHelper
|
||||
{
|
||||
private static Type resourceStorageType;
|
||||
private static MethodInfo getComponentsMethod;
|
||||
private static MethodInfo isPrivateMethod;
|
||||
private static MethodInfo addResourceStorageMethod;
|
||||
|
||||
private static readonly Assembly[] candidateAssemblies = new[]
|
||||
{
|
||||
typeof(Player).Assembly,
|
||||
typeof(World).Assembly,
|
||||
typeof(FreeResourceManager).Assembly,
|
||||
};
|
||||
|
||||
private static void EnsureInitialized()
|
||||
{
|
||||
if (resourceStorageType != null)
|
||||
return;
|
||||
|
||||
foreach (var assembly in candidateAssemblies)
|
||||
{
|
||||
if (assembly == null)
|
||||
continue;
|
||||
|
||||
resourceStorageType = assembly.GetType("Assets.Interface.IResourceStorage", false);
|
||||
if (resourceStorageType != null)
|
||||
break;
|
||||
}
|
||||
|
||||
if (resourceStorageType == null)
|
||||
return;
|
||||
|
||||
getComponentsMethod = typeof(Component).GetMethod("GetComponents", new[] { typeof(Type) });
|
||||
isPrivateMethod = resourceStorageType.GetMethod("IsPrivate", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
addResourceStorageMethod = typeof(FreeResourceManager).GetMethod("AddResourceStorage", new[] { resourceStorageType });
|
||||
}
|
||||
|
||||
public static Component[] GetStorages(Component owner)
|
||||
{
|
||||
EnsureInitialized();
|
||||
if (resourceStorageType == null || getComponentsMethod == null)
|
||||
return Array.Empty<Component>();
|
||||
|
||||
return (Component[])getComponentsMethod.Invoke(owner, new object[] { resourceStorageType });
|
||||
}
|
||||
|
||||
public static bool IsPrivate(Component storage)
|
||||
{
|
||||
EnsureInitialized();
|
||||
if (isPrivateMethod == null)
|
||||
return true;
|
||||
|
||||
return (bool)isPrivateMethod.Invoke(storage, null);
|
||||
}
|
||||
|
||||
public static void Register(Component storage)
|
||||
{
|
||||
EnsureInitialized();
|
||||
if (addResourceStorageMethod == null || FreeResourceManager.inst == null)
|
||||
return;
|
||||
|
||||
addResourceStorageMethod.Invoke(FreeResourceManager.inst, new object[] { storage });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
using KCM;
|
||||
using KCM.Enums;
|
||||
using KCM.Packets.Handlers;
|
||||
using KCM.StateManagement.Observers;
|
||||
using Steamworks;
|
||||
using UnityEngine;
|
||||
|
||||
@@ -154,7 +153,23 @@ namespace Riptide.Demos.Steam.PlayerHosted
|
||||
//NetworkManager.Singleton.StopServer();
|
||||
//NetworkManager.Singleton.DisconnectClient();
|
||||
SteamMatchmaking.LeaveLobby(lobbyId);
|
||||
Main.ResetMultiplayerState("LeaveLobby");
|
||||
|
||||
if (KCClient.client.IsConnected)
|
||||
KCClient.client.Disconnect();
|
||||
|
||||
Main.helper.Log("clear players");
|
||||
Main.kCPlayers.Clear();
|
||||
Main.clientSteamIds.Clear(); // Clear client-to-steam ID mapping
|
||||
Main.ClearVillagerPositionCache(); // Clear villager sync cache
|
||||
LobbyHandler.ClearPlayerList();
|
||||
LobbyHandler.ClearChatEntries();
|
||||
Main.helper.Log("end clear players");
|
||||
|
||||
// Reset loading state
|
||||
loadingSave = false;
|
||||
|
||||
if (KCServer.IsRunning)
|
||||
KCServer.server.Stop();
|
||||
|
||||
Main.TransitionTo(MenuState.ServerBrowser);
|
||||
ServerBrowser.registerServer = false;
|
||||
|
||||
@@ -51,24 +51,12 @@ namespace KCM.ServerLobby.LobbyChat
|
||||
{
|
||||
try
|
||||
{
|
||||
if (banner == null)
|
||||
return;
|
||||
|
||||
string steamId;
|
||||
if (!Main.clientSteamIds.TryGetValue(Client, out steamId))
|
||||
return;
|
||||
|
||||
KCPlayer player;
|
||||
if (!Main.kCPlayers.TryGetValue(steamId, out player) || player == null)
|
||||
return;
|
||||
Main.kCPlayers.TryGetValue(Main.GetPlayerByClientID(Client).steamId, out player);
|
||||
|
||||
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;
|
||||
var bannerTexture = World.inst.liverySets[player.banner].banners;
|
||||
|
||||
banner.texture = bannerTexture;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -21,15 +21,15 @@ namespace KCM.ServerLobby
|
||||
|
||||
public void Start()
|
||||
{
|
||||
banner = transform.Find("PlayerBanner").GetComponent<RawImage>();
|
||||
|
||||
SetValues();
|
||||
|
||||
InvokeRepeating("SetValues", 0, 0.25f);
|
||||
|
||||
banner = transform.Find("PlayerBanner").GetComponent<RawImage>();
|
||||
|
||||
transform.Find("PlayerBanner").GetComponent<Button>().onClick.AddListener(() =>
|
||||
{
|
||||
Main.TransitionTo(MenuState.NameAndBanner);
|
||||
Main.TransitionTo(MenuState.NameAndBanner);//ChooseBannerUI Hooks required, as well as townnameui
|
||||
});
|
||||
}
|
||||
|
||||
@@ -37,39 +37,14 @@ namespace KCM.ServerLobby
|
||||
{
|
||||
try
|
||||
{
|
||||
if (banner == null)
|
||||
{
|
||||
var bannerTransform = transform.Find("PlayerBanner");
|
||||
if (bannerTransform == null)
|
||||
return;
|
||||
banner = bannerTransform.GetComponent<RawImage>();
|
||||
if (banner == null)
|
||||
return;
|
||||
}
|
||||
|
||||
string steamId;
|
||||
if (!Main.clientSteamIds.TryGetValue(Client, out steamId))
|
||||
return;
|
||||
|
||||
KCPlayer player;
|
||||
if (!Main.kCPlayers.TryGetValue(steamId, out player) || player == null)
|
||||
return;
|
||||
Main.kCPlayers.TryGetValue(Main.GetPlayerByClientID(Client).steamId, out player);
|
||||
transform.Find("PlayerName").GetComponent<TextMeshProUGUI>().text = player.name;
|
||||
transform.Find("Ready").gameObject.SetActive(player.ready);
|
||||
|
||||
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;
|
||||
var bannerTexture = World.inst.liverySets[player.banner].banners;
|
||||
|
||||
banner.texture = bannerTexture;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -60,6 +60,8 @@ namespace KCM
|
||||
Falle
|
||||
}
|
||||
|
||||
bool awake = false;
|
||||
|
||||
public void Start()
|
||||
{
|
||||
Main.helper.Log("ServerLobby start called");
|
||||
@@ -219,21 +221,6 @@ 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,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using KCM.Packets;
|
||||
using KCM.Packets;
|
||||
using KCM.Packets.State;
|
||||
using KCM.StateManagement.Observers;
|
||||
using System;
|
||||
@@ -6,7 +6,6 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using static KCM.StateManagement.Observers.Observer;
|
||||
|
||||
namespace KCM.StateManagement.BuildingState
|
||||
@@ -23,72 +22,27 @@ namespace KCM.StateManagement.BuildingState
|
||||
{
|
||||
try
|
||||
{
|
||||
Observer observer = sender as Observer;
|
||||
if (observer == null)
|
||||
return;
|
||||
Observer observer = (Observer)sender;
|
||||
|
||||
Building building = observer.state as Building;
|
||||
if (building == null)
|
||||
return;
|
||||
Building building = (Building)observer.state;
|
||||
|
||||
//Main.helper.Log("Should send building network update for: " + building.UniqueName);
|
||||
|
||||
var t = building.transform;
|
||||
if (t == null)
|
||||
return;
|
||||
|
||||
Quaternion rotation = t.rotation;
|
||||
Vector3 globalPosition = t.position;
|
||||
Vector3 localPosition = t.localPosition;
|
||||
|
||||
if (t.childCount > 0)
|
||||
{
|
||||
try
|
||||
{
|
||||
var child = t.GetChild(0);
|
||||
if (child != null)
|
||||
{
|
||||
rotation = child.rotation;
|
||||
localPosition = child.localPosition;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
float resourceProgress = 0f;
|
||||
try
|
||||
{
|
||||
var field = building.GetType().GetField("resourceProgress", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance);
|
||||
if (field != null)
|
||||
{
|
||||
object value = field.GetValue(building);
|
||||
if (value is float)
|
||||
resourceProgress = (float)value;
|
||||
else if (value != null)
|
||||
resourceProgress = Convert.ToSingle(value);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
new BuildingStatePacket()
|
||||
{
|
||||
customName = building.customName,
|
||||
guid = building.guid,
|
||||
uniqueName = building.UniqueName,
|
||||
rotation = rotation,
|
||||
globalPosition = globalPosition,
|
||||
localPosition = localPosition,
|
||||
rotation = building.transform.GetChild(0).rotation,
|
||||
globalPosition = building.transform.position,
|
||||
localPosition = building.transform.GetChild(0).localPosition,
|
||||
built = building.IsBuilt(),
|
||||
placed = building.IsPlaced(),
|
||||
open = building.Open,
|
||||
doBuildAnimation = building.doBuildAnimation,
|
||||
constructionPaused = building.constructionPaused,
|
||||
constructionProgress = building.constructionProgress,
|
||||
resourceProgress = resourceProgress,
|
||||
resourceProgress = (float)building.GetType().GetField("resourceProgress", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(building),
|
||||
life = building.Life,
|
||||
ModifiedMaxLife = building.ModifiedMaxLife,
|
||||
yearBuilt = building.YearBuilt,
|
||||
|
||||
@@ -128,31 +128,6 @@ namespace KCM.StateManagement.Observers
|
||||
if (this.state == null)
|
||||
return;
|
||||
|
||||
// Unity uses "fake null" for destroyed objects. Since our state is stored as object,
|
||||
// we must explicitly detect that case to avoid exceptions + log spam.
|
||||
try
|
||||
{
|
||||
UnityEngine.Object unityObj = this.state as UnityEngine.Object;
|
||||
if (this.state is UnityEngine.Object && unityObj == null)
|
||||
{
|
||||
try { StateObserver.observers.Remove(this.state.GetHashCode()); } catch { }
|
||||
try
|
||||
{
|
||||
if (observerObject != null)
|
||||
UnityEngine.Object.Destroy(observerObject);
|
||||
else
|
||||
UnityEngine.Object.Destroy(this.gameObject);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
|
||||
if (!(currentMs - lastUpdate > updateInterval)) // Don't run if the update interval hasn't passed (default 100 milliseconds);
|
||||
return;
|
||||
|
||||
|
||||
@@ -12,24 +12,6 @@ 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()))
|
||||
|
||||
@@ -1,890 +0,0 @@
|
||||
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 const int VillagerSnapshotIntervalMs = 1000;
|
||||
|
||||
private static long lastResourceBroadcastMs;
|
||||
private static long lastVillagerValidationMs;
|
||||
private static long lastVillagerSnapshotMs;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
if ((now - lastVillagerSnapshotMs) >= VillagerSnapshotIntervalMs)
|
||||
{
|
||||
lastVillagerSnapshotMs = now;
|
||||
try
|
||||
{
|
||||
BroadcastVillagerSnapshot();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log("Error broadcasting villager snapshot");
|
||||
Main.helper.Log(ex.ToString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void SendResyncToClient(ushort toClient, string reason)
|
||||
{
|
||||
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
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private static void BroadcastVillagerSnapshot()
|
||||
{
|
||||
if (!KCServer.IsRunning)
|
||||
return;
|
||||
|
||||
if (KCServer.server.ClientCount == 0)
|
||||
return;
|
||||
|
||||
if (Villager.villagers == null || Villager.villagers.Count == 0)
|
||||
return;
|
||||
|
||||
List<Guid> guids = new List<Guid>();
|
||||
List<Vector3> positions = new List<Vector3>();
|
||||
const int maxVillagersPerSnapshot = 50;
|
||||
|
||||
for (int i = 0; i < Villager.villagers.Count && guids.Count < maxVillagersPerSnapshot; i++)
|
||||
{
|
||||
Villager villager = Villager.villagers.data[i];
|
||||
if (villager == null)
|
||||
continue;
|
||||
|
||||
guids.Add(villager.guid);
|
||||
positions.Add(villager.Pos);
|
||||
}
|
||||
|
||||
if (guids.Count == 0)
|
||||
return;
|
||||
|
||||
VillagerSnapshotPacket snapshot = new VillagerSnapshotPacket
|
||||
{
|
||||
guids = guids,
|
||||
positions = positions
|
||||
};
|
||||
|
||||
ushort exceptId = KCClient.client != null ? KCClient.client.Id : (ushort)0;
|
||||
snapshot.SendToAll(exceptId);
|
||||
}
|
||||
|
||||
public static void ApplyResourceSnapshot(List<int> resourceTypes, List<int> amounts)
|
||||
{
|
||||
if (resourceTypes == null || amounts == null)
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user