Compare commits

..

82 Commits

Author SHA1 Message Date
8b9c19186b fix: Update button container paths for consistency and improve fallback handling 2025-12-15 16:49:21 +01:00
99398b5479 fix: Improve logging for menu transitions and resolve top-level canvas references in ServerBrowser 2025-12-15 10:39:38 +01:00
270a92c617 fix: Update button template resolution and improve error logging for missing components 2025-12-15 09:56:55 +01:00
4d8279719c fix: Update button container path to reflect correct hierarchy and improve logging 2025-12-15 09:51:02 +01:00
3d5a53f0e2 fix: Enhance button container pathfinding and add null checks for ServerBrowser references 2025-12-15 09:49:37 +01:00
25f5af0b4d fix: Add null checks for MainMenuUI_T and TopLevelUICanvas in ServerBrowser to prevent instantiation errors 2025-12-15 09:40:11 +01:00
2ad605138e fix: Implement lazy initialization for Constants to prevent null reference errors 2025-12-15 09:36:27 +01:00
ca517be369 refactor: Remove BuildingRemoveHook to streamline building removal process 2025-12-15 09:35:39 +01:00
df1def69e4 fix: Restore correct namespace reference for packetId in BuildingRemovePacket 2025-12-15 09:32:00 +01:00
db850885f6 fix: Correct packetId reference in BuildingRemovePacket to use the appropriate Enums 2025-12-15 09:30:25 +01:00
71e1e09c75 feat: Enhance building removal logic to ensure correct player job list modifications and add fallback for Remove method 2025-12-15 09:27:05 +01:00
46ebeb1f80 feat: Enhance BuildingRemove logic to correctly manage Player.inst during building removal 2025-12-15 09:22:50 +01:00
7d06145a34 feat: Implement BuildingRemoveHook to manage building removal and prevent infinite loops
fix: Enhance SaveTransferPacket handling for out-of-order delivery and reset transfer state
2025-12-15 09:19:46 +01:00
fcf1ffac76 feat: Add BuildingRemovePacket to handle building removal requests in-game 2025-12-15 09:19:42 +01:00
40369ffe4b fix: Improve player retrieval logic and handle non-existent clients in PlayerEntryScript 2025-12-15 09:15:31 +01:00
fc089afcc0 fix: Enhance logging for save transfer process and completion checks in SaveTransferPacket 2025-12-15 00:06:36 +01:00
cb82d3706f fix: Add missing using directive for Riptide.Demos.Steam.PlayerHosted in SaveTransferPacket 2025-12-14 23:58:51 +01:00
12a207989e fix: Reset save transfer state and streamline loading logic in SaveTransferPacket and StartGame 2025-12-14 23:56:41 +01:00
4afcaccf75 fix: Reset save transfer state and update progress bar calculations 2025-12-14 23:54:49 +01:00
8f13282e04 fix: Improve job type initialization and handle game state during loading 2025-12-14 23:45:12 +01:00
0d7d989f76 fix: Change log warning to info for invalid seed handling in world generation 2025-12-14 23:24:57 +01:00
1cc3042781 feat: Implement various multiplayer stability and synchronization fixes
This commit addresses several critical issues reported by the user to improve the stability and synchronization of the Kingdoms and Castles multiplayer mod.

Key changes include:

- Improved Lobby Stability: Fixed NullReferenceException during lobby entry.
- Enhanced Session Cleanup: Refined disconnection logic to prevent Steamworks shutdown and enable seamless new game starts without client restarts.
- Optimized Building Synchronization: Implemented a throttling mechanism for building state updates to reduce network traffic.
- Resolved Villager Freezing: Introduced a null check for destroyed observed buildings to prevent synchronization cascades.
- Fixed Map Desynchronization: Ensured the host reliably sends the world seed to all clients before game start.
- Reliable Save Game Transfer: Switched save file chunk transfer to reliable messaging mode to prevent incomplete save loads.
- Addressed Compilation Issues: Resolved all compilation errors and warnings that arose from the implemented fixes.
2025-12-14 23:22:57 +01:00
181936e3d4 Refactor seed handling in game start logic and improve packet sending reliability 2025-12-14 23:22:34 +01:00
62db70c1c4 Refactor packet sending to use SendReliable method for improved reliability and remove unnecessary logging 2025-12-14 23:18:23 +01:00
36acbb57c5 Enhance reliability of chunk data transmission and update world seed handling on game start 2025-12-14 23:13:43 +01:00
76f1033bd2 Fix null reference handling in building state updates 2025-12-14 23:00:23 +01:00
26b5f1201e Refactor player entry initialization and optimize building state update logic 2025-12-14 22:27:43 +01:00
9ee675ac19 Implement multiplayer session cleanup on client disconnection 2025-12-14 22:12:50 +01:00
3124f82a2f alap 2025-12-14 21:08:19 +01:00
3a7b81bfd7 save 2025-12-14 21:04:22 +01:00
c4eb7e944d Fix map synchronization between host and clients
Problem: Clients received WorldSeed packet before ServerSettings,
causing world generation with wrong mapSize/mapBias/mapRivers
parameters. Same seed but different parameters = different maps.

Solution: Include all map parameters directly in WorldSeed packet:
- WorldSize (map size)
- WorldType (map bias - continents/islands/etc)
- WorldRivers (river/lake density)

Now packet order doesn't matter - WorldSeed has everything needed
for identical world generation across all clients.

Changes:
- WorldSeed.cs: Add map parameters, set before Generate()
- ClientConnected.cs: Send full world params to joining clients
- ServerLobbyScript.cs: Send full params on new world generation
- Added [WORLD SYNC] debug logging

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 21:00:15 +01:00
4057cf37c5 sex 2025-12-14 20:52:55 +01:00
fc467f4af8 Fix StartGame NullReferenceException in multiplayer
Remove MainMenuMode.StartGame() reflection call that expected
"Choose Your Map" screen state. Clients never see this screen
in multiplayer, causing null references. Now transitions directly
to playing mode like save loading does.

Fixes: NullReferenceException at MainMenuMode.StartGame()

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 20:52:44 +01:00
dbc0328c6f Merge pull request 'codex-3' (#4) from codex-3 into main
Reviewed-on: #4
2025-12-14 20:29:45 +01:00
5f67f488f6 Fix compilation warnings and namespace issue
Fixed issues:
1. Added missing using directive for KCM.Packets namespace
   - Fixes ShowModal class not found error in Main.cs

2. Removed unused 'callTree' variable (Main.cs:460)
   - Variable was defined but never used

3. Removed unused 'awake' field (ServerLobbyScript.cs:63)
   - Field was assigned but never used

All compilation warnings resolved.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 20:12:08 +01:00
b0f790cb6e Fix 3 critical bugs: server shutdown, building placement crashes
Fixed issues:
1. Server now stops when host returns to menu (Main.cs:342-356)
   - Notifies clients with "Host disconnected" modal
   - Prevents server from running in background

2. PlayerAddBuildingHook NullReferenceException (Main.cs:762-806)
   - Added comprehensive null checks for reflection fields
   - Added array bounds validation for landMass index
   - Added registry initialization checks
   - Fixes 98% building placement failure rate

3. IndexOutOfRangeException in WorldPlace (WorldPlace.cs:167-183)
   - LandMassNames array auto-expands when needed
   - Defensive code prevents index out of bounds errors

Updated README.md:
- Removed fixed issues from bug tracker
- Added "Fixed Issues" section documenting changes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 20:06:53 +01:00
deb0c0ad92 Document follow-up steps 2025-12-14 16:33:45 +01:00
10d20e29ad Kódban található KRITIKUS hibák részletes dokumentációja
A README-ben leírt bugok pontos helyének és okának meghatározása
a kódbázisban, javítási javaslatokkal:

DOKUMENTÁLT HIBÁK:
1. Server nem áll le menüváltáskor
   - Hely: KCServer.cs (hiányzó logika)
   - Ok: OnApplicationQuit csak app bezáráskor hívódik
   - Javítás: TransitionToHook-ban server.Stop() hívás

2. PlayerAddBuildingHook NullReferenceException (LEGKRITIKUSABB)
   - Hely: Main.cs:764
   - Ok: landMassBuildingRegistry.data[landMass] null/hibás indexelés
   - Eredmény: 55/56 épület fail (98%!)
   - Javítás: NULL check + array méret ellenőrzés

3. IndexOutOfRangeException WorldPlace-ben
   - Hely: WorldPlace.cs:167-168
   - Ok: LandMassNames tömb túl kicsi
   - Okozó: #2 hiba miatt building nem adódik hozzá → tömb nem nő
   - Javítás: Védekező kód + #2 javítása

ÖSSZEFÜGGÉSEK:
A három hiba cascade failure-t okoz: server fut menüben →
packeteket fogad → building placement fail (#2) →
IndexOutOfRange (#3) → 98% épület nem jelenik meg!

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 16:11:48 +01:00
a3409a2d5c asd 2025-12-14 16:07:35 +01:00
5b0f957295 Dokumentálva a README.md fájlban található hibák
Új szekció hozzáadva a dokumentációs hibák részletes leírásával:
- Nyelvtani hibák (hiányzó ékezetek 6 helyen)
- Strukturális hiányosságok (bevezető, útmutatók)
- Konzisztencia problémák

A hibák NEM lettek javítva, csak dokumentálva, hogy hol találhatók
és hogyan kell őket javítani. Prioritások meghatározva.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-14 16:04:41 +01:00
faaedcc0fd Update bug tracker with complete log analysis
KRITIKUS: 55/56 (98%) building placement FAILS!
- 56 BUILDING PLACEMENT START
- Only 1 BUILDING PLACEMENT END
- 55 "Error in add building hook" NullReferenceException
- 9 IndexOutOfRangeException
- 2 StartGame.Start() crashes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-14 16:00:25 +01:00
d87c12384e Update bug tracker with issues found from output.txt analysis
KRITIKUS bugs added:
- Server nem all le amikor host kilep menube
- Kliens nem lesz kidobva host kilepesekor
- Packetek erkeznek menu-ben

Building placement errors:
- PlayerAddBuildingHook NullReferenceException (~50+ occurrences)
- IndexOutOfRangeException in WorldPlace

Added log analysis timeline from 15:39-15:56 session.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-14 15:58:02 +01:00
560a985323 Add Keep placement bug to tracker
Multiple Keeps can be placed on same island, causing original player to lose their Keep.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-14 15:42:26 +01:00
490e0d74e6 Add bug tracker README with known issues and status
🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-14 15:37:48 +01:00
c4e25f6c12 Add advanced sync logging for debugging host-client sync issues
- Add LogSync() helper method in Main.cs for consistent sync logging
- Log all packet send/receive events in PacketHandler and Packet classes
- Add detailed building placement logging in WorldPlace.cs (all properties, final state)
- Add building state update logging in BuildingStatePacket.cs
- Add building state send logging in BuildingStateManager.cs

All sync logs are prefixed with [SYNC] for easy filtering.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-14 15:35:57 +01:00
6b014c72db Fix building materials and road/aqueduct rotation for remote players
Add UpdateMaterialSelection() and UpdateRotation() calls after building
placement to fix visual issues when host views client's buildings.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-14 15:27:41 +01:00
c6ca1abc54 hm 2025-12-14 15:25:29 +01:00
c3e79c9adf Merge pull request 'codex-2' (#3) from codex-2 into main
Reviewed-on: #3
2025-12-14 15:10:39 +01:00
dca0140aab Improve multiplayer sync stability 2025-12-14 14:51:46 +01:00
55e3cd57e7 yes 2025-12-14 14:45:37 +01:00
4685bc61c2 asd 2025-12-14 14:41:35 +01:00
7a14303353 Initialize player entry banner before SetValues 2025-12-14 14:21:20 +01:00
a918262d99 Always point save dir to multiplayer folder 2025-12-14 14:18:03 +01:00
87f65320c0 Broadcast villager positions from FixedUpdate 2025-12-14 14:15:45 +01:00
97bbf059a9 Specify villager update signature 2025-12-14 14:14:21 +01:00
aa6fb797c2 Merge branch 'codex' 2025-12-14 14:12:29 +01:00
739eba8289 Cast villager to Component via object 2025-12-14 13:33:44 +01:00
1e6f09df18 Guard villager movement snapshot with component 2025-12-14 13:31:49 +01:00
15cad47b52 Broadcast villager movement from server 2025-12-14 13:30:23 +01:00
c074a86423 Find resource storage type via known assemblies 2025-12-14 13:20:48 +01:00
1035f06884 Lookup resource storage type safely 2025-12-14 13:12:08 +01:00
b05c3415f2 codex talán fixálta idk 2025-12-14 13:09:32 +01:00
b02af4d0c7 asd 2025-12-14 12:59:18 +01:00
5dba8137c3 Fix: Remove AddBuilding from ProcessBuildingHook
The original PlayerSaveData.Unpack calls AddBuilding, PlaceFromLoad,
and UnpackStage2 after ProcessBuilding returns. The hook should only
create and initialize the building, not call these methods.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-14 11:58:00 +01:00
b3d7108574 Debug: Add detailed logging to ProcessBuildingHook
Added step-by-step logging to identify where building load fails:
- GetPlaceableByUniqueName result
- Each initialization step
- Exception details if any step fails

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-14 11:50:31 +01:00
ce1c067fca Fix: Add PlaceFromLoad and UnpackStage2 in ProcessBuildingHook
The hook was missing the critical World.inst.PlaceFromLoad() call which:
- Places building in world cells
- Sets up pathing data for villager navigation
- Registers building properly

Also added UnpackStage2() for complete building initialization.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-14 11:45:59 +01:00
5d086776cf Fix: Add missing BakePathing() call in PlayerAddBuildingHook
The hook was skipping the original AddBuilding method but not calling
BakePathing(), which is required for villager pathfinding to work.
Without this, villagers cannot find paths to buildings.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-14 11:42:20 +01:00
89586ad8df Fix: Remove TeleportTo calls that break villager AI/movement
- Remove post-load villager TeleportTo refresh (breaks pathfinding)
- Remove periodic villager position sync (TeleportTo interrupts movement)
- Keep ClearVillagerPositionCache for API compatibility

The TeleportTo calls were resetting villager AI state and preventing
them from continuing their movement/work.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-14 11:39:03 +01:00
8f3d83e807 Fix: Compile errors - variable naming and missing using
- Rename lambda variable 'v' to 'w' to avoid conflict with local 'v'
- Rename local Villager 'v' to 'newVillager' for clarity
- Add missing 'using Assets.Interface' for IResourceStorage

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-14 11:02:01 +01:00
dd17030e56 Fix: Add periodic villager position sync from server
- Server syncs villager positions every ~3 seconds to clients
- Only syncs villagers that moved more than 0.5 units (bandwidth optimization)
- Maintains position cache to detect movement
- Clears cache on lobby leave to prevent stale data

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-14 10:58:37 +01:00
8d599e13ad Fix: Add post-load fixes for resources and villagers
- Re-register all resource storages after load to fix missing resources
- Refresh building pathing for all players
- Teleport villagers to their position to reset stuck pathfinding/AI
- Add comprehensive error handling and logging

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-14 10:56:51 +01:00
eab7931f52 Fix: Add position sync for villagers and duplicate check
- Add position property to AddVillagerPacket
- Teleport villager to correct position on client
- Add duplicate guid check to prevent double villager creation
- Send position from Main.cs hook

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-14 10:52:39 +01:00
7d6c915b49 Fix: Prevent duplicate building placement via guid check
- Check if building with same guid already exists before placing
- Prevents buildings overlapping from network packet retries
- Logs skip when duplicate detected

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-14 10:51:53 +01:00
f7fc5a3969 Fix: Proper session cleanup in LeaveLobby
- Clear clientSteamIds dictionary on disconnect
- Reset loadingSave flag to false
- Prevents stale data when rejoining servers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-14 10:51:21 +01:00
2f42cf9366 Fix: Server event handler duplication and null safety
- Remove static constructor that registered MessageReceived handler
- Add proper cleanup in StartServer() before creating new instance
- Add null checks in IsRunning, Update(), and OnApplicationQuit()
- Prevents double event handler registration on reconnect

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-14 10:50:57 +01:00
888c807b96 nah 2025-12-14 10:50:05 +01:00
bd12485112 Add CLAUDE.md documentation for Claude Code
Provides architecture overview, packet system docs, and common patterns
for future development assistance.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-14 10:49:52 +01:00
f03e13236f asd 2025-12-14 01:45:02 +01:00
60d509344c fix? 2025-12-14 01:42:22 +01:00
121df60b57 fix? 2025-12-14 01:35:14 +01:00
3fbaac2346 na még 1x 2025-12-14 01:27:26 +01:00
0e87108b56 sexy fix 2025-12-14 01:25:29 +01:00
37 changed files with 736 additions and 2669 deletions

View File

@@ -0,0 +1,12 @@
{
"permissions": {
"allow": [
"Bash(wc:*)",
"Bash(find:*)",
"Bash(ls:*)",
"Bash(git add:*)",
"Bash(git commit:*)",
"Bash(tail:*)"
]
}
}

5
.gitignore vendored
View File

@@ -1,7 +1,5 @@
# Logs / local debug output
output*.txt
*.log
/log.txt
# OS junk
.DS_Store
@@ -24,3 +22,6 @@ Desktop.ini
**/*.pdb
/.claude
/*.png
/*.txt
/*.jpg

View File

@@ -1,10 +0,0 @@
using System;
namespace KCM.Attributes
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class NoServerRelayAttribute : Attribute
{
}
}

View File

@@ -15,19 +15,20 @@ namespace KCM
/// </summary>
public static class Constants
{
public static readonly MainMenuMode MainMenuMode = GameState.inst.mainMenuMode;
public static readonly PlayingMode PlayingMode = GameState.inst.playingMode;
public static readonly World World = GameState.inst.world;
// Use lazy initialization to avoid null reference when GameState isn't ready yet
public static MainMenuMode MainMenuMode => GameState.inst?.mainMenuMode;
public static PlayingMode PlayingMode => GameState.inst?.playingMode;
public static World World => GameState.inst?.world;
#region "UI"
public static readonly Transform MainMenuUI_T = MainMenuMode.mainMenuUI.transform;
public static readonly GameObject MainMenuUI_O = MainMenuMode.mainMenuUI;
public static Transform MainMenuUI_T => MainMenuMode?.mainMenuUI?.transform;
public static GameObject MainMenuUI_O => MainMenuMode?.mainMenuUI;
/* public static readonly Transform TopLevelUI_T = MainMenuUI_T.parent;
public static readonly GameObject TopLevelUI_O = MainMenuUI_T.parent.gameObject;*/
public static readonly Transform ChooseModeUI_T = MainMenuMode.chooseModeUI.transform;
public static readonly GameObject ChooseModeUI_O = MainMenuMode.chooseModeUI;
public static Transform ChooseModeUI_T => MainMenuMode?.chooseModeUI?.transform;
public static GameObject ChooseModeUI_O => MainMenuMode?.chooseModeUI;
#endregion
}

View File

@@ -18,11 +18,8 @@ namespace KCM.Enums
KingdomName = 32,
StartGame = 33,
WorldSeed = 34,
Building = 50,
BuildingOnPlacement = 51,
World = 70,
WorldPlace = 71,
FellTree = 72,
@@ -45,9 +42,6 @@ namespace KCM.Enums
SetupInitialWorkers = 89,
VillagerTeleportTo = 90,
PlaceKeepRandomly = 91,
ResyncRequest = 92,
ResourceSnapshot = 93,
BuildingSnapshot = 94,
VillagerSnapshot = 95
BuildingRemove = 92
}
}

View File

@@ -36,11 +36,10 @@ namespace KCM
private static void Client_Disconnected(object sender, DisconnectedEventArgs e)
{
Main.CleanupMultiplayerSession();
Main.helper.Log("Client disconnected event start");
try
{
Main.ResetMultiplayerState("Client disconnected");
if (e.Message != null)
{
Main.helper.Log(e.Message.ToString());
@@ -78,19 +77,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 +89,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);
}

View File

@@ -21,9 +21,6 @@ namespace KCM
public static Server server = new Server(Main.steamServer);
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"));
@@ -53,7 +50,6 @@ namespace KCM
}
ev.Client.CanQualityDisconnect = false;
ev.Client.MaxSendAttempts = 50;
Main.helper.Log("Client ID is: " + ev.Client.Id);
@@ -62,42 +58,15 @@ namespace KCM
server.ClientDisconnected += (obj, ev) =>
{
try
{
var playerName = $"Client {ev.Client.Id}";
string steamId;
if (Main.clientSteamIds.TryGetValue(ev.Client.Id, out steamId) && !string.IsNullOrEmpty(steamId))
{
KCPlayer player;
if (Main.kCPlayers.TryGetValue(steamId, out player) && player != null && !string.IsNullOrEmpty(player.name))
playerName = player.name;
Main.kCPlayers.Remove(steamId);
}
Main.clientSteamIds.Remove(ev.Client.Id);
new ChatSystemMessage()
{
Message = $"{playerName} has left the server.",
Message = $"{Main.GetPlayerByClientID(ev.Client.Id).name} 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.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.helper.Log($"Client disconnected. {ev.Reason}");
}
catch (Exception ex)
{
Main.helper.Log("Error handling client disconnect");
Main.helper.Log(ex.ToString());
}
};
Main.helper.Log($"Listening on port 7777. Max {LobbyHandler.ServerSettings.MaxPlayers} clients.");
@@ -131,69 +100,6 @@ namespace KCM
private void Update()
{
server.Update();
ProcessSaveTransfers();
KCM.StateManagement.Sync.SyncManager.ServerUpdate();
}
private static void ProcessSaveTransfers()
{
if (!KCServer.IsRunning)
return;
if (saveTransferQueues.Count == 0)
return;
var clients = saveTransferQueues.Keys.ToList();
foreach (var clientId in clients)
{
Queue<SaveTransferPacket> queue;
if (!saveTransferQueues.TryGetValue(clientId, out queue) || queue == null)
continue;
int sentThisUpdate = 0;
while (sentThisUpdate < SaveTransferPacketsPerUpdatePerClient && queue.Count > 0)
{
var packet = queue.Dequeue();
packet.Send(clientId);
sentThisUpdate++;
}
if (queue.Count == 0)
saveTransferQueues.Remove(clientId);
}
}
public static void EnqueueSaveTransfer(ushort toClient, byte[] bytes)
{
if (bytes == null)
return;
int chunkSize = 900;
int sent = 0;
int totalChunks = (int)Math.Ceiling((double)bytes.Length / chunkSize);
var queue = new Queue<SaveTransferPacket>(totalChunks);
for (int i = 0; i < totalChunks; i++)
{
int currentChunkSize = Math.Min(chunkSize, bytes.Length - sent);
var chunk = new byte[currentChunkSize];
Array.Copy(bytes, sent, chunk, 0, currentChunkSize);
queue.Enqueue(new SaveTransferPacket()
{
saveSize = bytes.Length,
saveDataChunk = chunk,
chunkId = i,
chunkSize = chunk.Length,
saveDataIndex = sent,
totalChunks = totalChunks
});
sent += currentChunkSize;
}
saveTransferQueues[toClient] = queue;
Main.helper.Log($"Queued {totalChunks} save data chunks for client {toClient}");
}
private void OnApplicationQuit()

View File

@@ -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
{
@@ -25,42 +24,16 @@ namespace KCM.LoadSaveOverrides
Main.helper.Log($"Saving data for {Main.kCPlayers.Count} ({KCServer.server.ClientCount}) players.");
//this.PlayerSaveData = new PlayerSaveDataOverride().Pack(Player.inst);
foreach (var player in Main.kCPlayers.Values)
{
try
{
if (player == null)
continue;
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($"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);
Main.helper.Log($"{players[player.steamId] == null}");
}
catch (Exception ex)
{
string steamId = (player != null && player.steamId != null) ? player.steamId : string.Empty;
string name = (player != null && player.name != null) ? player.name : string.Empty;
Main.helper.Log($"Error packing player data for save (steamId={steamId}, name={name})");
Main.helper.Log(ex.ToString());
}
}
this.WorldSaveData = new World.WorldSaveData().Pack(World.inst);
this.FishSystemSaveData = new FishSystem.FishSystemSaveData().Pack(FishSystem.inst);
@@ -71,25 +44,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.AIBrainsSaveData = new AIBrainsContainer.SaveData().Pack(AIBrainsContainer.inst);
this.SiegeMonsterSaveData = new SiegeMonster.SiegeMonsterSaveData().Pack(null);
this.CartSystemSaveData = new CartSystem.CartSystemSaveData().Pack(CartSystem.inst);
@@ -102,8 +57,10 @@ namespace KCM.LoadSaveOverrides
public override object Unpack(object obj)
{
//original Player reset was up here
foreach (var kvp in players)
{
KCPlayer player;
if (!Main.kCPlayers.TryGetValue(kvp.Key, out player))
@@ -118,6 +75,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 +87,10 @@ namespace KCM.LoadSaveOverrides
}
this.TownNameSaveData.Unpack(TownNameUI.inst);
//TownNameUI.inst.townName = kingdomNames[Main.PlayerSteamID];
TownNameUI.inst.SetTownName(kingdomNames[Main.PlayerSteamID]);
Main.helper.Log("Unpacking player data");
Player.PlayerSaveData clientPlayerData = null;
@@ -142,9 +104,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 +120,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 +207,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 +220,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,32 +228,28 @@ 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]}");

1012
Main.cs

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,91 @@
using System;
using System.Reflection;
using UnityEngine;
namespace KCM.Packets.Game.GameBuilding
{
public class BuildingRemovePacket : Packet
{
public override ushort packetId => (ushort)KCM.Enums.Packets.BuildingRemove;
// Flag to prevent infinite loop when removing buildings from packet
public static bool isProcessingPacket = false;
public Guid guid { get; set; }
public override void HandlePacketClient()
{
if (clientId == KCClient.client.Id) return;
Main.helper.Log($"Received building remove packet for guid {guid} from {player.name}");
// Try to find the building in the player who owns it
Building building = player.inst.GetBuilding(guid);
if (building == null)
{
// Try to find it in any player's buildings
foreach (var kcp in Main.kCPlayers.Values)
{
building = kcp.inst.GetBuilding(guid);
if (building != null) break;
}
}
if (building == null)
{
Main.helper.Log($"Building with guid {guid} not found on client, may already be removed.");
return;
}
try
{
Main.helper.Log($"Removing building {building.UniqueName} at {building.transform.position}");
// Set flag to prevent sending packet back
isProcessingPacket = true;
// Set Player.inst to the correct player for this building
// This ensures the removal modifies the correct player's job lists
Player originalPlayer = Player.inst;
Player correctPlayer = Main.GetPlayerByBuilding(building);
if (correctPlayer != null)
{
Player.inst = correctPlayer;
}
// Use reflection to call the Remove method from the game assembly
MethodInfo removeMethod = typeof(Building).GetMethod("Remove", BindingFlags.Public | BindingFlags.Instance);
if (removeMethod != null)
{
removeMethod.Invoke(building, null);
}
else
{
// Fallback: destroy the building GameObject directly
Main.helper.Log("Remove method not found, using Destroy fallback");
building.destroyedWhileInPlay = true;
UnityEngine.Object.Destroy(building.gameObject);
}
// Restore original Player.inst
Player.inst = originalPlayer;
isProcessingPacket = false;
}
catch (Exception e)
{
isProcessingPacket = false;
Main.helper.Log($"Error removing building: {e.Message}");
Main.helper.Log(e.StackTrace);
}
}
public override void HandlePacketServer()
{
// Forward the remove packet to all other clients
SendToAll(clientId);
}
}
}

View File

@@ -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()
{
}
}
}

View File

@@ -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,76 +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
{
// Apply speed setting
SpeedControlUI.inst.SetSpeed(speed);
// Handle pause/unpause state
if (isPaused && Time.timeScale > 0)
{
// Game should be paused
Time.timeScale = 0f;
Main.helper.Log("Game paused via network sync");
}
else if (!isPaused && Time.timeScale == 0)
{
// Game should be unpaused - restore speed
Time.timeScale = 1f;
SpeedControlUI.inst.SetSpeed(speed);
Main.helper.Log("Game unpaused via network sync");
}
// Force AI system update when speed changes
if (speed > 0)
{
try
{
// Force villager system refresh to ensure they continue working
if (VillagerSystem.inst != null)
{
// Use reflection to call any refresh methods on VillagerSystem
var villagerSystemType = typeof(VillagerSystem);
var refreshMethods = villagerSystemType.GetMethods(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)
.Where(m => m.Name.Contains("Refresh") || m.Name.Contains("Update") || m.Name.Contains("Restart"));
foreach (var method in refreshMethods)
{
if (method.GetParameters().Length == 0)
{
try
{
method.Invoke(VillagerSystem.inst, null);
Main.helper.Log($"Called VillagerSystem.{method.Name} for speed change");
}
catch { }
}
}
}
Main.helper.Log($"AI systems refreshed for speed change to {speed}");
}
catch (Exception e)
{
Main.helper.Log("Error refreshing AI on speed change: " + e.Message);
}
}
}
catch (Exception e)
{
Main.helper.Log("Error handling SetSpeed packet: " + e.Message);
}
}
public override void HandlePacketServer()
{
// Server relay is handled automatically by PacketHandler unless [NoServerRelay] is used.
//throw new NotImplementedException();
}
}
}

View File

@@ -35,7 +35,6 @@ namespace KCM.Packets.Handlers
if (!KCServer.IsRunning)
{
Main.kCPlayers.Clear();
Main.clientSteamIds.Clear();
}
}
catch (Exception ex)

View File

@@ -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,8 +137,6 @@ namespace KCM.Packets.Handlers
{
packet.HandlePacketServer();
bool shouldRelay = packet.GetType().GetCustomAttributes(typeof(NoServerRelayAttribute), inherit: true).Length == 0;
if (shouldRelay)
((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);

View File

@@ -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]);
}

View File

@@ -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;
}
}

View File

@@ -4,7 +4,7 @@ using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using KCM.StateManagement.Observers;
using Riptide.Demos.Steam.PlayerHosted;
using static KCM.Main;
namespace KCM.Packets.Lobby
@@ -18,14 +18,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,111 +30,83 @@ namespace KCM.Packets.Lobby
public override void HandlePacketClient()
{
bool initialisingTransfer = !loadingSave ||
saveData == null ||
saveData.Length != saveSize ||
chunksReceived == null ||
chunksReceived.Length != totalChunks;
if (initialisingTransfer)
// Initialize on first chunk OR if arrays aren't properly sized yet
// This handles out-of-order packet delivery
if (!loadingSave || saveData.Length != saveSize || chunksReceived.Length != totalChunks)
{
Main.helper.Log("Save Transfer started!");
Main.helper.Log($"Save Transfer initializing. saveSize={saveSize}, totalChunks={totalChunks}");
loadingSave = true;
received = 0;
StateObserver.ClearAll();
saveData = new byte[saveSize];
chunksReceived = new bool[totalChunks];
received = 0;
if (ServerLobbyScript.LoadingSave != null)
ServerLobbyScript.LoadingSave.SetActive(true);
}
if (chunkId < 0 || chunkId >= totalChunks)
// Skip if we already received this chunk (duplicate packet)
if (chunksReceived[chunkId])
{
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}");
Main.helper.Log($"[SaveTransfer] Duplicate chunk {chunkId} received, skipping.");
return;
}
Array.Copy(saveDataChunk, 0, saveData, saveDataIndex, saveDataChunk.Length);
chunksReceived[chunkId] = true;
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";
Main.helper.Log($"[SaveTransfer] Processed chunk {chunkId}/{totalChunks}. Received: {received} bytes of {saveSize}.");
if (chunkId + 1 == totalChunks)
// Update progress bar
if (saveSize > 0)
{
Main.helper.Log($"Received last save transfer packet.");
float savePercent = (float)received / (float)saveSize;
string receivedKB = ((float)received / 1000f).ToString("0.00");
string totalKB = ((float)saveSize / 1000f).ToString("0.00");
Main.helper.Log(WhichIsNotComplete());
ServerLobbyScript.ProgressBar.fillAmount = savePercent;
ServerLobbyScript.ProgressBarText.text = (savePercent * 100).ToString("0.00") + "%";
ServerLobbyScript.ProgressText.text = $"{receivedKB} KB / {totalKB} KB";
}
else
{
ServerLobbyScript.ProgressBar.fillAmount = 0f;
ServerLobbyScript.ProgressBarText.text = "0.00%";
ServerLobbyScript.ProgressText.text = "0.00 KB / 0.00 KB";
}
// Check if all chunks have been received
if (IsTransferComplete())
{
// Handle completed transfer here
Main.helper.Log("Save Transfer complete!");
// Reset the loading state before processing
loadingSave = false;
LoadSaveLoadHook.saveBytes = saveData;
LoadSaveLoadHook.memoryStreamHook = true;
LoadSave.Load();
try
{
Main.SetMultiplayerSaveLoadInProgress(true);
LoadSaveLoadHook.saveContainer.Unpack(null);
}
finally
{
Main.SetMultiplayerSaveLoadInProgress(false);
}
GameState.inst.SetNewMode(GameState.inst.playingMode);
LobbyManager.loadingSave = false;
try
{
RunPostLoadRebuild("Save transfer complete");
}
catch
{
}
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;
// Reset static state for next transfer
ResetTransferState();
}
}
public static void ResetTransferState()
{
saveData = new byte[1];
chunksReceived = new bool[1];
}
loadingSave = false;
received = 0;
}
public static bool IsTransferComplete()
@@ -150,19 +114,6 @@ namespace KCM.Packets.Lobby
return chunksReceived.All(x => x == true);
}
public static string WhichIsNotComplete()
{
string notComplete = "";
for (int i = 0; i < chunksReceived.Length; i++)
{
if (!chunksReceived[i])
{
notComplete += i + ", ";
}
}
return notComplete;
}
public override void HandlePacketServer()
{
}

View File

@@ -18,15 +18,9 @@ namespace KCM.Packets.Lobby
{
Main.helper.Log(GameState.inst.mainMenuMode.ToString());
// Hide server lobby
Main.TransitionTo((MenuState)200);
// This is run when user clicks "accept" on choose your map screeen
try
{
int desiredSpeed = 1;
if (!LobbyManager.loadingSave)
{
SpeedControlUI.inst.SetSpeed(0);
@@ -40,66 +34,29 @@ namespace KCM.Packets.Lobby
Main.helper.Log(ex.ToString());
}
SpeedControlUI.inst.SetSpeed(desiredSpeed);
}
else
{
LobbyManager.loadingSave = false;
GameState.inst.SetNewMode(GameState.inst.playingMode);
SpeedControlUI.inst.SetSpeed(desiredSpeed);
}
SpeedControlUI.inst.SetSpeed(0);
}
catch (Exception ex)
{
// Handle exception here
Main.helper.Log(ex.Message.ToString());
Main.helper.Log(ex.ToString());
}
}
public override void HandlePacketClient()
{
if (!LobbyManager.loadingSave)
{
Start();
}
else
{
ServerLobbyScript.LoadingSave.SetActive(true);
}
}
public override void HandlePacketServer()
{
//Start();
/*AIBrainsContainer.PreStartAIConfig aiConfig = new AIBrainsContainer.PreStartAIConfig();
int count = 0;
for (int i = 0; i < RivalKingdomSettingsUI.inst.rivalItems.Length; i++)
{
RivalItemUI r = RivalKingdomSettingsUI.inst.rivalItems[i];
bool flag = r.Enabled && !r.Locked;
if (flag)
{
count++;
}
}
int idx = 0;
aiConfig.startData = new AIBrainsContainer.PreStartAIConfig.AIStartData[count];
for (int j = 0; j < RivalKingdomSettingsUI.inst.rivalItems.Length; j++)
{
RivalItemUI item = RivalKingdomSettingsUI.inst.rivalItems[j];
bool flag2 = item.Enabled && !item.Locked;
if (flag2)
{
aiConfig.startData[idx] = new AIBrainsContainer.PreStartAIConfig.AIStartData();
aiConfig.startData[idx].landmass = item.flag.landmass;
aiConfig.startData[idx].bioCode = item.bannerIdx;
aiConfig.startData[idx].personalityKey = PersonalityCollection.aiPersonalityKeys[0];
aiConfig.startData[idx].skillLevel = item.GetSkillLevel();
idx++;
}
}
AIBrainsContainer.inst.aiStartInfo = aiConfig;
bool isControllerActive = GamepadControl.inst.isControllerActive;
if (isControllerActive)
{
ConsoleCursorMenu.inst.PrepForGamepad();
}*/
}
}
}

View File

@@ -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)
{

View File

@@ -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,33 @@ 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
}.SendReliable(clientId);
packetsSent++;
sent += chunk.Length;
}
Main.helper.Log($"Sent {packetsSent} save data chunks to client");
}
else
{

View File

@@ -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());
}
}
}
}

View File

@@ -38,8 +38,7 @@ namespace KCM.Packets.Network
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;
Main.kCPlayers.Add(Main.PlayerSteamID, new KCPlayer(KCClient.inst.Name, clientId, Main.PlayerSteamID));
Player.inst.PlayerLandmassOwner.teamId = clientId * 10 + 2;

View File

@@ -11,21 +11,22 @@ 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;
if (Main.kCPlayers.TryGetValue(Main.GetPlayerByClientID(clientId).steamId, out p))
return p;
else
{
Main.helper.Log($"Error getting player from packet {packetId} {this.GetType().Name} from {clientId}");
}
KCPlayer player;
if (Main.kCPlayers.TryGetValue(steamId, out player))
return player;
Main.helper.Log($"Error getting player from packet {packetId} {GetType().Name} from {clientId}");
return null;
}
}
@@ -103,6 +104,37 @@ namespace KCM.Packets
}
}
public void SendReliable(ushort toClient)
{
try
{
if (KCServer.IsRunning && toClient != 0)
{
KCServer.server.Send(PacketHandler.SerialisePacket(this), toClient, true);
}
}
catch (Exception ex)
{
Main.helper.Log($"Error sending reliable packet {packetId} {this.GetType().Name} from {clientId}");
Main.helper.Log("----------------------- Main exception -----------------------");
Main.helper.Log(ex.ToString());
Main.helper.Log("----------------------- Main message -----------------------");
Main.helper.Log(ex.Message);
Main.helper.Log("----------------------- Main stacktrace -----------------------");
Main.helper.Log(ex.StackTrace);
if (ex.InnerException != null)
{
Main.helper.Log("----------------------- Inner exception -----------------------");
Main.helper.Log(ex.InnerException.ToString());
Main.helper.Log("----------------------- Inner message -----------------------");
Main.helper.Log(ex.InnerException.Message);
Main.helper.Log("----------------------- Inner stacktrace -----------------------");
Main.helper.Log(ex.InnerException.StackTrace);
}
}
}
public abstract void HandlePacketServer();
public abstract void HandlePacketClient();
}

View File

@@ -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()
{
}
}
}

View File

@@ -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; }

View File

@@ -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()
{
}
}
}

View File

@@ -1,75 +1,38 @@
# KCM (Kingdoms and Castles Multiplayer)
# Kingdoms and Castles Multiplayer Mod Fixes
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.
This document summarizes the fixes and improvements implemented to enhance the stability and functionality of the multiplayer mod for Kingdoms and Castles.
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).
## Implemented Fixes:
## Mit szinkronizál a mod? (jelenlegi állapot)
### 1. Improved Lobby Stability
- **Issue:** Previously, joining a multiplayer lobby could lead to an immediate crash (NullReferenceException in `PlayerEntryScript.cs`).
- **Fix:** Corrected the initialization order of UI components in `PlayerEntryScript.cs` to prevent NullReferenceExceptions, ensuring stable lobby entry.
**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.
### 2. Enhanced Session Cleanup
- **Issue:** Users previously had to restart the entire game after leaving a multiplayer session to join or host a new one. This was due to residual game state and an aggressive cleanup that inadvertently shut down Steamworks.
- **Fix:** Implemented a comprehensive `CleanupMultiplayerSession()` routine in `Main.cs`. This routine now properly resets static mod data (player lists, client/server states), and, crucially, no longer destroys the core `KCMSteamManager` (Steamworks API manager). This allows for seamless transitions between multiplayer sessions without game restarts.
**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.
### 3. Optimized Building Synchronization Performance
- **Issue:** Rapid changes in building state (e.g., during construction) could generate excessive network traffic, potentially contributing to "poor connection" issues.
- **Fix:** Implemented a throttling mechanism in `BuildingStateManager.cs`. Building state updates are now limited to 10 times per second per building, significantly reducing network spam while maintaining visual fluidity.
**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).
### 4. Resolved Villager Freezing
- **Issue:** Villagers would sometimes freeze during gameplay, despite other game elements functioning correctly. This was caused by the game attempting to synchronize the state of already destroyed building components, leading to a cascade of errors.
- **Fix:** Added a robust null check in `BuildingStateManager.cs`. If an observed building has been destroyed, its associated observer is now properly de-registered (by destroying its GameObject), preventing further errors and ensuring continuous game logic for villagers and other entities. This also handles cases where buildings are replaced (e.g., construction completed).
**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.
### 5. Fixed Map Desynchronization
- **Issue:** When starting a new multiplayer game, clients often generated a different map than the host, even if the seed was specified. This was due to the host not sending the definitive world seed at the critical moment.
- **Fix:** Modified `ServerLobbyScript.cs` to ensure that when the host clicks "Start Game", the current world seed (either from UI input or newly generated) is explicitly sent to all clients via a `WorldSeed` packet *before* the game starts. This guarantees all players generate the exact same map.
## Mi nincs (még) rendesen szinkronizálva? (gyakori desync okok)
### 6. Reliable Save Game Transfer
- **Issue:** Loading a saved multiplayer game would often fail for clients, resulting in an incomplete save file and desynchronized gameplay. This occurred because save file chunks were sent unreliably over the network.
- **Fix:** Changed the save game chunk transfer in `ClientConnected.cs` to use Riptide's `Reliable` message send mode. This ensures that all parts of the save file are guaranteed to arrive at the client, allowing for complete and successful save game loading.
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.
### 7. Compilation Errors & Warnings Addressed
- All reported compilation errors and warnings (including issues with `Packet.Send` overloads and `World.SeedFromText`) have been investigated and resolved, ensuring the mod compiles cleanly.
## Mit érdemes még hozzáadni? (roadmap)
## Pending Task:
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 50100 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).
### Resource Synchronization
- **Goal:** Implement synchronization for player resources (Gold, Wood, Stone, Food) to ensure all players see consistent resource counts.
- **Status:** Awaiting confirmation from the user regarding the exact `FreeResourceType` enum names (`Wood`, `Stone`, `Food`) to proceed with implementation.

View File

@@ -1,7 +1,6 @@
using KCM;
using KCM.Enums;
using KCM.Packets.Handlers;
using KCM.StateManagement.Observers;
using Steamworks;
using UnityEngine;
@@ -154,7 +153,20 @@ 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();
LobbyHandler.ClearPlayerList();
LobbyHandler.ClearChatEntries();
Main.helper.Log("end clear players");
if (KCServer.IsRunning)
KCServer.server.Stop();
Main.TransitionTo(MenuState.ServerBrowser);
ServerBrowser.registerServer = false;

View File

@@ -299,13 +299,35 @@ namespace KCM
try
{
GameObject kcmUICanvas = Instantiate(Constants.MainMenuUI_T.Find("TopLevelUICanvas").gameObject);
if (Constants.MainMenuUI_T == null)
{
Main.helper.Log("MainMenuUI_T is null in ServerBrowser");
return;
}
var topLevelCanvas = ResolveMenuCanvas();
if (topLevelCanvas == null)
{
Main.helper.Log("Failed to resolve top-level menu canvas in ServerBrowser");
return;
}
GameObject kcmUICanvas = Instantiate(topLevelCanvas.gameObject);
for (int i = 0; i < kcmUICanvas.transform.childCount; i++)
Destroy(kcmUICanvas.transform.GetChild(i).gameObject);
kcmUICanvas.name = "KCMUICanvas";
kcmUICanvas.transform.SetParent(Constants.MainMenuUI_T);
kcmUICanvas.transform.SetParent(Constants.MainMenuUI_T, false);
kcmUICanvas.transform.SetAsLastSibling();
kcmUICanvas.SetActive(false);
var canvasComponent = kcmUICanvas.GetComponent<Canvas>();
if (canvasComponent != null)
{
canvasComponent.overrideSorting = true;
canvasComponent.sortingOrder = 999;
}
KCMUICanvas = kcmUICanvas.transform;
@@ -322,6 +344,8 @@ namespace KCM
serverLobbyPlayerRef = serverLobbyRef.transform.Find("Container/PlayerList/Viewport/Content");
serverLobbyChatRef = serverLobbyRef.transform.Find("Container/PlayerChat/Viewport/Content");
serverLobbyRef.SetActive(false);
serverBrowserRef.transform.SetAsLastSibling();
serverLobbyRef.transform.SetAsLastSibling();
//browser.transform.position = new Vector3(0, 0, 0);
@@ -435,6 +459,29 @@ namespace KCM
}
}
private Transform ResolveMenuCanvas()
{
string[] candidatePaths =
{
"TopLevelUICanvas",
"TopLevel",
"MainMenu/TopLevel/TopLevelUICanvas",
"MainMenu/TopLevel"
};
foreach (var path in candidatePaths)
{
var transform = Constants.MainMenuUI_T.Find(path);
if (transform != null)
{
Main.helper.Log($"ServerBrowser: using canvas path '{path}'.");
return transform;
}
}
return null;
}
private void Preload(KCModHelper helper)
{
helper.Log("Hello?");

View File

@@ -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;
var bannerTexture = World.inst.liverySets[player.banner].banners;
if (player.banner < 0 || player.banner >= World.inst.liverySets.Count)
return;
banner.texture = World.inst.liverySets[player.banner].banners;
banner.texture = bannerTexture;
}
catch (Exception ex)
{

View File

@@ -29,7 +29,7 @@ namespace KCM.ServerLobby
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,21 @@ namespace KCM.ServerLobby
{
try
{
if (banner == null)
// First check if the client still exists
if (!Main.TryGetPlayerByClientID(Client, out KCPlayer player) || player == null)
{
var bannerTransform = transform.Find("PlayerBanner");
if (bannerTransform == null)
return;
banner = bannerTransform.GetComponent<RawImage>();
if (banner == null)
// Client no longer exists, stop the repeating invoke and destroy this entry
CancelInvoke("SetValues");
Destroy(gameObject);
return;
}
string steamId;
if (!Main.clientSteamIds.TryGetValue(Client, out steamId))
return;
transform.Find("PlayerName").GetComponent<TextMeshProUGUI>().text = player.name;
transform.Find("Ready").gameObject.SetActive(player.ready);
KCPlayer player;
if (!Main.kCPlayers.TryGetValue(steamId, out player) || player == null)
return;
var bannerTexture = World.inst.liverySets[player.banner].banners;
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;
banner.texture = bannerTexture;
}
catch (Exception ex)
{

View File

@@ -142,7 +142,6 @@ namespace KCM
{
Main.helper.Log("Disable all");
//StartGameButton.gameObject.SetActive(false);
StartGameButton.onClick.RemoveAllListeners();
StartGameButton.GetComponentInChildren<TextMeshProUGUI>().text = "Ready";
StartGameButton.onClick.AddListener(() =>
@@ -187,6 +186,32 @@ namespace KCM
StartGameButton.GetComponentInChildren<TextMeshProUGUI>().text = "Start";
StartGameButton.onClick.AddListener(() =>
{
int definitiveSeed;
if (string.IsNullOrWhiteSpace(WorldSeed.text))
{
World.inst.Generate();
definitiveSeed = World.inst.seed;
}
else
{
if (int.TryParse(WorldSeed.text, out int parsedSeed))
{
definitiveSeed = parsedSeed;
World.inst.Generate(definitiveSeed);
}
else
{
Main.helper.Log($"Invalid seed '{WorldSeed.text}' entered. Generating a random seed.");
World.inst.Generate();
definitiveSeed = World.inst.seed;
}
}
new WorldSeed()
{
Seed = definitiveSeed
}.SendToAll(KCClient.client.Id);
new StartGame().SendToAll();
if (PlacementType.value == 0 && !LobbyManager.loadingSave)
@@ -219,21 +244,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,

View File

@@ -13,6 +13,8 @@ namespace KCM.StateManagement.BuildingState
{
public class BuildingStateManager
{
private static readonly Dictionary<Guid, float> lastUpdateTime = new Dictionary<Guid, float>();
private const float UpdateInterval = 0.1f; // 10 times per second
public static void BuildingStateChanged(object sender, StateUpdateEventArgs args)
{
@@ -23,72 +25,47 @@ namespace KCM.StateManagement.BuildingState
{
try
{
Observer observer = sender as Observer;
if (observer == null)
return;
Observer observer = (Observer)sender;
Building building = (Building)observer.state;
Building building = observer.state as Building;
if (building == null)
{
if(observer != null)
{
UnityEngine.Object.Destroy(observer.gameObject);
}
return;
}
Guid guid = building.guid;
if (lastUpdateTime.ContainsKey(guid) && Time.time < lastUpdateTime[guid] + UpdateInterval)
{
return; // Not time to update yet
}
if (!lastUpdateTime.ContainsKey(guid))
lastUpdateTime.Add(guid, Time.time);
else
lastUpdateTime[guid] = Time.time;
//Main.helper.Log("Should send building network update for: " + building.UniqueName);
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,

View File

@@ -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;

View File

@@ -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()))

View File

@@ -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);
}
}
}
}

View File

@@ -15,6 +15,11 @@ namespace KCM.UI
class KaC_Button
{
public Button Button = null;
private static readonly string[] ButtonPaths =
{
"TopLevelUICanvas/TopLevel/Body/ButtonContainer/New",
"MainMenu/TopLevel/Body/ButtonContainer/New" // fallback for older versions
};
public string Name
{
@@ -84,14 +89,18 @@ namespace KCM.UI
set => Transform.SetSiblingIndex(value);
}
public KaC_Button(Transform parent = null)
{
Button b = Constants.MainMenuUI_T.Find("TopLevelUICanvas/TopLevel/Body/ButtonContainer/New").GetComponent<Button>();
public KaC_Button(Transform parent = null) : this(null, parent) { }
if (parent == null)
Button = GameObject.Instantiate(b);
else
Button = GameObject.Instantiate(b, parent);
public KaC_Button(Button b, Transform parent = null)
{
var templateButton = ResolveTemplateButton(b);
if (templateButton == null)
throw new InvalidOperationException("Template button not found in main menu UI.");
Button = parent == null
? GameObject.Instantiate(templateButton)
: GameObject.Instantiate(templateButton, parent);
foreach (Localize Localize in Button.GetComponentsInChildren<Localize>())
GameObject.Destroy(Localize);
@@ -99,20 +108,27 @@ namespace KCM.UI
Button.onClick = new Button.ButtonClickedEvent();
}
public KaC_Button(Button b, Transform parent = null)
private static Button ResolveTemplateButton(Button providedButton)
{
if (b == null)
b = Constants.MainMenuUI_T.Find("TopLevelUICanvas/TopLevel/Body/ButtonContainer/New").GetComponent<Button>();
if (providedButton != null)
return providedButton;
if (parent == null)
Button = GameObject.Instantiate(b);
else
Button = GameObject.Instantiate(b, parent);
foreach (var path in ButtonPaths)
{
var transform = Constants.MainMenuUI_T?.Find(path);
if (transform == null)
continue;
foreach (Localize Localize in Button.GetComponentsInChildren<Localize>())
GameObject.Destroy(Localize);
var button = transform.GetComponent<Button>();
if (button != null)
{
Main.helper?.Log($"Using menu button template at '{path}'.");
return button;
}
}
Button.onClick = new Button.ButtonClickedEvent();
Main.helper?.Log("Failed to find menu button template for KaC_Button.");
return null;
}
public override string ToString()