Compare commits
36 Commits
deb0c0ad92
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 8b9c19186b | |||
| 99398b5479 | |||
| 270a92c617 | |||
| 4d8279719c | |||
| 3d5a53f0e2 | |||
| 25f5af0b4d | |||
| 2ad605138e | |||
| ca517be369 | |||
| df1def69e4 | |||
| db850885f6 | |||
| 71e1e09c75 | |||
| 46ebeb1f80 | |||
| 7d06145a34 | |||
| fcf1ffac76 | |||
| 40369ffe4b | |||
| fc089afcc0 | |||
| cb82d3706f | |||
| 12a207989e | |||
| 4afcaccf75 | |||
| 8f13282e04 | |||
| 0d7d989f76 | |||
| 1cc3042781 | |||
| 181936e3d4 | |||
| 62db70c1c4 | |||
| 36acbb57c5 | |||
| 76f1033bd2 | |||
| 26b5f1201e | |||
| 9ee675ac19 | |||
| 3124f82a2f | |||
| 3a7b81bfd7 | |||
| c4eb7e944d | |||
| 4057cf37c5 | |||
| fc467f4af8 | |||
| dbc0328c6f | |||
| 5f67f488f6 | |||
| b0f790cb6e |
12
.claude/settings.local.json
Normal file
12
.claude/settings.local.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(wc:*)",
|
||||
"Bash(find:*)",
|
||||
"Bash(ls:*)",
|
||||
"Bash(git add:*)",
|
||||
"Bash(git commit:*)",
|
||||
"Bash(tail:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
104
CLAUDE.md
104
CLAUDE.md
@@ -1,104 +0,0 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a **Kingdoms and Castles Multiplayer Mod** that adds multiplayer functionality to the game using:
|
||||
- **Riptide Networking** library for low-level networking
|
||||
- **Steam P2P** transport for NAT traversal
|
||||
- **Harmony** for non-invasive game modification via patches/hooks
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `Main.cs` | Entry point, Harmony patches, all game event hooks |
|
||||
| `KCClient.cs` | Client-side networking wrapper around Riptide.Client |
|
||||
| `KCServer.cs` | Server-side networking, client management |
|
||||
| `KCPlayer.cs` | Player data container (id, steamId, inst, kingdomName) |
|
||||
|
||||
### Networking Layer
|
||||
|
||||
```
|
||||
Riptide.Client/Server
|
||||
└── SteamClient/SteamServer (Steam P2P transport)
|
||||
└── KCClient/KCServer wrappers
|
||||
└── PacketHandler (serialization/routing)
|
||||
```
|
||||
|
||||
- Port: 7777, Max clients: 25
|
||||
- Team ID formula: `clientId * 10 + 2`
|
||||
|
||||
### Packet System
|
||||
|
||||
Located in `/Packets/`:
|
||||
- Base class: `Packet.cs` with `Send()`, `SendToAll()`, `HandlePacketClient()`, `HandlePacketServer()`
|
||||
- `PacketHandler.cs` uses reflection for automatic serialization based on property names (alphabetical order)
|
||||
- Packet IDs defined in `Enums/Packets.cs`
|
||||
|
||||
Key packet ranges:
|
||||
- 25-34: Lobby (chat, player list, settings)
|
||||
- 70-79: World/building updates
|
||||
- 85: Save transfer (chunked)
|
||||
- 87-90: Building state, villagers
|
||||
|
||||
### State Synchronization
|
||||
|
||||
- **Buildings**: Observer pattern in `StateManagement/BuildingState/` - monitors field changes every 100ms, sends updates every 300ms
|
||||
- **Villagers**: Event-based sync via Harmony hooks on `VillagerSystem.AddVillager`, `Villager.TeleportTo`
|
||||
- **Save/Load**: Custom `MultiplayerSaveContainer` extends `LoadSaveContainer`, stores per-player data
|
||||
|
||||
### Harmony Hooks Pattern
|
||||
|
||||
All hooks check call stack to prevent infinite loops:
|
||||
```csharp
|
||||
if (new StackFrame(3).GetMethod().Name.Contains("HandlePacket"))
|
||||
return; // Skip if called by network handler
|
||||
```
|
||||
|
||||
### Key Dictionaries
|
||||
|
||||
```csharp
|
||||
Main.kCPlayers // Dictionary<steamId, KCPlayer>
|
||||
Main.clientSteamIds // Dictionary<clientId, steamId>
|
||||
```
|
||||
|
||||
## Common Issues & Patterns
|
||||
|
||||
### Player Resolution
|
||||
```csharp
|
||||
Main.GetPlayerByClientID(clientId) // clientId -> KCPlayer
|
||||
Main.GetPlayerByTeamID(teamId) // teamId -> Player.inst
|
||||
Main.GetPlayerByBuilding(building) // building -> owner Player
|
||||
```
|
||||
|
||||
### Building Ownership
|
||||
Buildings are associated with players via `LandmassOwner.teamId`. Use `building.TeamID()` to determine owner.
|
||||
|
||||
### Save Directory
|
||||
Multiplayer saves go to: `Application.persistentDataPath + "/Saves/Multiplayer"`
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
/Attributes - Custom packet attributes
|
||||
/Enums - Packet types, menu states
|
||||
/LoadSaveOverrides - MultiplayerSaveContainer
|
||||
/Packets - All network packets
|
||||
/Riptide - Networking library
|
||||
/RiptideSteamTransport - Steam P2P adapter, LobbyManager
|
||||
/StateManagement - Observer pattern for sync
|
||||
/ServerLobby - Lobby UI
|
||||
/ServerBrowser - Server discovery
|
||||
/UI - Custom UI elements
|
||||
```
|
||||
|
||||
## Known Architecture Limitations
|
||||
|
||||
1. Static `Client`/`Server` instances can cause issues on reconnect
|
||||
2. Call stack checking for loop prevention is fragile
|
||||
3. No conflict resolution - last-write-wins
|
||||
4. Villager sync is event-based only, no continuous state updates
|
||||
15
Constants.cs
15
Constants.cs
@@ -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
|
||||
|
||||
}
|
||||
|
||||
@@ -18,11 +18,8 @@ namespace KCM.Enums
|
||||
KingdomName = 32,
|
||||
StartGame = 33,
|
||||
WorldSeed = 34,
|
||||
|
||||
|
||||
Building = 50,
|
||||
BuildingOnPlacement = 51,
|
||||
|
||||
World = 70,
|
||||
WorldPlace = 71,
|
||||
FellTree = 72,
|
||||
@@ -44,6 +41,7 @@ namespace KCM.Enums
|
||||
AddVillager = 88,
|
||||
SetupInitialWorkers = 89,
|
||||
VillagerTeleportTo = 90,
|
||||
PlaceKeepRandomly = 91
|
||||
PlaceKeepRandomly = 91,
|
||||
BuildingRemove = 92
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ namespace KCM
|
||||
|
||||
private static void Client_Disconnected(object sender, DisconnectedEventArgs e)
|
||||
{
|
||||
Main.CleanupMultiplayerSession();
|
||||
Main.helper.Log("Client disconnected event start");
|
||||
try
|
||||
{
|
||||
|
||||
36
KCServer.cs
36
KCServer.cs
@@ -18,22 +18,18 @@ namespace KCM
|
||||
{
|
||||
public class KCServer : MonoBehaviour
|
||||
{
|
||||
public static Server server = null;
|
||||
public static Server server = new Server(Main.steamServer);
|
||||
public static bool started = false;
|
||||
|
||||
static KCServer()
|
||||
{
|
||||
//server.registerMessageHandler(typeof(KCServer).GetMethod("ClientJoined"));
|
||||
|
||||
server.MessageReceived += PacketHandler.HandlePacketServer;
|
||||
}
|
||||
|
||||
public static void StartServer()
|
||||
{
|
||||
// Stop and cleanup existing server if running
|
||||
if (server != null)
|
||||
{
|
||||
if (server.IsRunning)
|
||||
{
|
||||
server.Stop();
|
||||
}
|
||||
// Unsubscribe old event handlers to prevent memory leaks
|
||||
server.MessageReceived -= PacketHandler.HandlePacketServer;
|
||||
}
|
||||
|
||||
server = new Server(Main.steamServer);
|
||||
server.MessageReceived += PacketHandler.HandlePacketServer;
|
||||
|
||||
@@ -99,26 +95,16 @@ namespace KCM
|
||||
}
|
||||
}*/
|
||||
|
||||
public static bool IsRunning { get { return server != null && server.IsRunning; } }
|
||||
public static bool IsRunning { get { return server.IsRunning; } }
|
||||
|
||||
private void Update()
|
||||
{
|
||||
if (server != null)
|
||||
server.Update();
|
||||
server.Update();
|
||||
}
|
||||
|
||||
private void OnApplicationQuit()
|
||||
{
|
||||
if (server != null && server.IsRunning)
|
||||
{
|
||||
new ShowModal
|
||||
{
|
||||
title = "Host disconnected",
|
||||
message = "The host has left the game."
|
||||
}.SendToAll();
|
||||
|
||||
server.Stop();
|
||||
}
|
||||
server.Stop();
|
||||
}
|
||||
|
||||
private void Preload(KCModHelper helper)
|
||||
|
||||
@@ -24,6 +24,7 @@ namespace KCM.LoadSaveOverrides
|
||||
|
||||
Main.helper.Log($"Saving data for {Main.kCPlayers.Count} ({KCServer.server.ClientCount}) players.");
|
||||
|
||||
//this.PlayerSaveData = new PlayerSaveDataOverride().Pack(Player.inst);
|
||||
foreach (var player in Main.kCPlayers.Values)
|
||||
{
|
||||
Main.helper.Log($"Attempting to pack data for: " + player.name + $"({player.steamId})");
|
||||
@@ -56,6 +57,7 @@ namespace KCM.LoadSaveOverrides
|
||||
|
||||
public override object Unpack(object obj)
|
||||
{
|
||||
//original Player reset was up here
|
||||
foreach (var kvp in players)
|
||||
{
|
||||
|
||||
@@ -86,6 +88,7 @@ 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");
|
||||
@@ -252,47 +255,6 @@ namespace KCM.LoadSaveOverrides
|
||||
Main.helper.Log($"Setting kingdom name to: {kingdomNames[Main.PlayerSteamID]}");
|
||||
TownNameUI.inst.SetTownName(kingdomNames[Main.PlayerSteamID]);
|
||||
|
||||
// Post-load fixes for multiplayer
|
||||
Main.helper.Log("Running post-load multiplayer fixes...");
|
||||
|
||||
// Fix 1: Re-register all resource storages for all players
|
||||
Main.helper.Log("Re-registering resource storages...");
|
||||
foreach (var kcPlayer in Main.kCPlayers.Values)
|
||||
{
|
||||
if (kcPlayer.inst == null) continue;
|
||||
|
||||
foreach (var building in kcPlayer.inst.Buildings.data)
|
||||
{
|
||||
if (building == null) continue;
|
||||
|
||||
// Re-register resource storages
|
||||
var storages = KCM.ResourceStorageHelper.GetStorages(building);
|
||||
foreach (var storage in storages)
|
||||
{
|
||||
if (storage != null && !KCM.ResourceStorageHelper.IsPrivate(storage))
|
||||
{
|
||||
try
|
||||
{
|
||||
KCM.ResourceStorageHelper.Register(storage);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Main.helper.Log($"Error re-registering storage for {building.UniqueName}: {e.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Refresh building pathing
|
||||
try
|
||||
{
|
||||
building.BakePathing();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
Main.helper.Log("Post-load multiplayer fixes complete.");
|
||||
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
|
||||
387
Main.cs
387
Main.cs
@@ -1,5 +1,6 @@
|
||||
using Assets.Code;
|
||||
using Assets.Code.UI;
|
||||
using Assets.Interface;
|
||||
using Harmony;
|
||||
using KCM.Enums;
|
||||
using KCM.LoadSaveOverrides;
|
||||
@@ -14,7 +15,6 @@ using KCM.Packets.Game.GameWorld;
|
||||
using KCM.Packets.Handlers;
|
||||
using KCM.Packets.Lobby;
|
||||
using KCM.StateManagement.BuildingState;
|
||||
using KCM.Packets.State;
|
||||
using KCM.StateManagement.Observers;
|
||||
using KCM.UI;
|
||||
using Newtonsoft.Json;
|
||||
@@ -54,17 +54,24 @@ namespace KCM
|
||||
|
||||
public static Dictionary<string, KCPlayer> kCPlayers = new Dictionary<string, KCPlayer>();
|
||||
public static Dictionary<ushort, string> clientSteamIds = new Dictionary<ushort, string>();
|
||||
public static Dictionary<Guid, BuildingStatePacket> pendingBuildingStatePackets = new Dictionary<Guid, BuildingStatePacket>();
|
||||
|
||||
// Advanced sync logging helper
|
||||
public static void LogSync(string message)
|
||||
{
|
||||
helper.Log($"[SYNC] {message}");
|
||||
}
|
||||
|
||||
public static KCPlayer GetPlayerByClientID(ushort clientId)
|
||||
{
|
||||
return kCPlayers[clientSteamIds[clientId]];
|
||||
if (TryGetPlayerByClientID(clientId, out KCPlayer player))
|
||||
{
|
||||
return player;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static bool TryGetPlayerByClientID(ushort clientId, out KCPlayer player)
|
||||
{
|
||||
player = null;
|
||||
if (clientSteamIds.TryGetValue(clientId, out string steamId))
|
||||
{
|
||||
return kCPlayers.TryGetValue(steamId, out player);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static Player GetPlayerByTeamID(int teamId) // Need to replace building / production types so that the correct player is used. IResourceStorage and IResourceProvider, and jobs
|
||||
@@ -89,26 +96,6 @@ namespace KCM
|
||||
return Player.inst;
|
||||
}
|
||||
|
||||
public static void QueuePendingBuildingState(BuildingStatePacket packet)
|
||||
{
|
||||
if (packet == null)
|
||||
return;
|
||||
|
||||
pendingBuildingStatePackets[packet.guid] = packet;
|
||||
}
|
||||
|
||||
public static void ApplyPendingBuildingState(Building building)
|
||||
{
|
||||
if (building == null)
|
||||
return;
|
||||
|
||||
if (pendingBuildingStatePackets.TryGetValue(building.guid, out var pending))
|
||||
{
|
||||
pending.ApplyToBuilding(building);
|
||||
pendingBuildingStatePackets.Remove(building.guid);
|
||||
}
|
||||
}
|
||||
|
||||
public static Player GetPlayerByBuilding(Building building)
|
||||
{
|
||||
try
|
||||
@@ -133,11 +120,51 @@ namespace KCM
|
||||
public static string PlayerSteamID = SteamUser.GetSteamID().ToString();
|
||||
|
||||
public static KCMSteamManager KCMSteamManager = null;
|
||||
public static LobbyManager lobbyManager = null;
|
||||
public static SteamServer steamServer = new SteamServer();
|
||||
public static Riptide.Transports.Steam.SteamClient steamClient = new Riptide.Transports.Steam.SteamClient(steamServer);
|
||||
|
||||
public static ushort currentClient = 0;
|
||||
|
||||
public static void CleanupMultiplayerSession()
|
||||
{
|
||||
if (helper == null) return; // Avoid running if mod is not fully initialized
|
||||
|
||||
helper.Log("--- Starting Multiplayer Session Cleanup ---");
|
||||
|
||||
// Disconnect client
|
||||
if (KCClient.client != null && KCClient.client.IsConnected)
|
||||
{
|
||||
helper.Log("Disconnecting client...");
|
||||
KCClient.client.Disconnect();
|
||||
}
|
||||
|
||||
// Stop server
|
||||
if (KCServer.server != null && KCServer.IsRunning)
|
||||
{
|
||||
helper.Log("Stopping server...");
|
||||
KCServer.server.Stop();
|
||||
}
|
||||
|
||||
// Clear player lists
|
||||
if (kCPlayers.Count > 0 || clientSteamIds.Count > 0)
|
||||
{
|
||||
helper.Log($"Clearing {kCPlayers.Count} KCPlayer entries and {clientSteamIds.Count} client steam IDs.");
|
||||
kCPlayers.Clear();
|
||||
clientSteamIds.Clear();
|
||||
}
|
||||
|
||||
// Destroy persistent managers
|
||||
if (lobbyManager != null)
|
||||
{
|
||||
helper.Log("Destroying LobbyManager.");
|
||||
Destroy(lobbyManager.gameObject);
|
||||
lobbyManager = null;
|
||||
}
|
||||
|
||||
helper.Log("--- Multiplayer Session Cleanup Finished ---");
|
||||
}
|
||||
|
||||
#region "SceneLoaded"
|
||||
private void SceneLoaded(KCModHelper helper)
|
||||
{
|
||||
@@ -150,14 +177,9 @@ namespace KCM
|
||||
KCMSteamManager = new GameObject("KCMSteamManager").AddComponent<KCMSteamManager>();
|
||||
DontDestroyOnLoad(KCMSteamManager);
|
||||
|
||||
var lobbyManager = new GameObject("LobbyManager").AddComponent<LobbyManager>();
|
||||
lobbyManager = new GameObject("LobbyManager").AddComponent<LobbyManager>();
|
||||
DontDestroyOnLoad(lobbyManager);
|
||||
|
||||
//SteamFriends.InviteUserToGame(new CSteamID(76561198036307537), "test");
|
||||
//SteamMatchmaking.lobby
|
||||
|
||||
//Main.helper.Log($"Timer duration for hazardpay {Player.inst.hazardPayWarmup.Duration}");
|
||||
|
||||
try
|
||||
{
|
||||
|
||||
@@ -167,24 +189,62 @@ namespace KCM
|
||||
|
||||
Main.helper.Log(JsonConvert.SerializeObject(World.inst.mapSizeDefs, Formatting.Indented));
|
||||
|
||||
KaC_Button serverBrowser = new KaC_Button(Constants.MainMenuUI_T.Find("TopLevelUICanvas/TopLevel/Body/ButtonContainer/New").parent)
|
||||
// Check if MainMenuUI_T is available
|
||||
if (Constants.MainMenuUI_T == null)
|
||||
{
|
||||
Main.helper.Log("MainMenuUI_T is null, cannot create Multiplayer button");
|
||||
return;
|
||||
}
|
||||
|
||||
// Debug: Log the UI structure to find the correct path
|
||||
Main.helper.Log($"MainMenuUI_T name: {Constants.MainMenuUI_T.name}");
|
||||
Main.helper.Log($"MainMenuUI_T children count: {Constants.MainMenuUI_T.childCount}");
|
||||
for (int i = 0; i < Constants.MainMenuUI_T.childCount; i++)
|
||||
{
|
||||
var child = Constants.MainMenuUI_T.GetChild(i);
|
||||
Main.helper.Log($" Child {i}: {child.name}");
|
||||
for (int j = 0; j < child.childCount; j++)
|
||||
{
|
||||
Main.helper.Log($" SubChild {j}: {child.GetChild(j).name}");
|
||||
}
|
||||
}
|
||||
|
||||
// Correct path based on debug output: MainMenuUI -> TopLevelUICanvas -> TopLevel -> Body -> ButtonContainer -> New
|
||||
var buttonContainer = Constants.MainMenuUI_T.Find("TopLevelUICanvas/TopLevel/Body/ButtonContainer/New");
|
||||
if (buttonContainer == null)
|
||||
{
|
||||
Main.helper.Log("Button container not found at TopLevelUICanvas/TopLevel/Body/ButtonContainer/New");
|
||||
return;
|
||||
}
|
||||
Main.helper.Log($"Found button container at: {buttonContainer.name}");
|
||||
|
||||
var templateButton = buttonContainer.GetComponent<Button>();
|
||||
if (templateButton == null)
|
||||
{
|
||||
Main.helper.Log("Template button on container is missing Button component.");
|
||||
return;
|
||||
}
|
||||
|
||||
KaC_Button serverBrowser = new KaC_Button(templateButton, buttonContainer.parent)
|
||||
{
|
||||
Name = "Multiplayer",
|
||||
Text = "Multiplayer",
|
||||
FirstSibling = true,
|
||||
OnClick = () =>
|
||||
{
|
||||
//Constants.MainMenuUI_T.Find("TopLevelUICanvas/TopLevel").gameObject.SetActive(false);
|
||||
Main.helper?.Log("Multiplayer button clicked");
|
||||
SfxSystem.PlayUiSelect();
|
||||
|
||||
//ServerBrowser.serverBrowserRef.SetActive(true);
|
||||
TransitionTo(MenuState.ServerBrowser);
|
||||
}
|
||||
};
|
||||
serverBrowser.Transform.SetSiblingIndex(2);
|
||||
|
||||
|
||||
Destroy(Constants.MainMenuUI_T.Find("TopLevelUICanvas/TopLevel/Body/ButtonContainer/Kingdom Share").gameObject);
|
||||
var kingdomShare = Constants.MainMenuUI_T.Find("TopLevelUICanvas/TopLevel/Body/ButtonContainer/Kingdom Share")
|
||||
?? Constants.MainMenuUI_T.Find("MainMenu/TopLevel/Body/ButtonContainer/Kingdom Share");
|
||||
if (kingdomShare != null)
|
||||
{
|
||||
Destroy(kingdomShare.gameObject);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -209,59 +269,41 @@ namespace KCM
|
||||
#endregion
|
||||
|
||||
public static int FixedUpdateInterval = 0;
|
||||
private readonly Dictionary<Guid, Vector3> villagerPositionCache = new Dictionary<Guid, Vector3>();
|
||||
private float villagerBroadcastAccumulator = 0f;
|
||||
private const float VillagerBroadcastInterval = 0.25f;
|
||||
private const float VillagerMovementThreshold = 0.3f;
|
||||
|
||||
public static void ClearVillagerPositionCache()
|
||||
{
|
||||
// Kept for API compatibility with LobbyManager
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
FixedUpdateInterval++;
|
||||
BroadcastVillagerMovements();
|
||||
}
|
||||
|
||||
private void BroadcastVillagerMovements()
|
||||
{
|
||||
if (!KCServer.IsRunning || KCServer.server == null)
|
||||
return;
|
||||
|
||||
villagerBroadcastAccumulator += Time.fixedDeltaTime;
|
||||
if (villagerBroadcastAccumulator < VillagerBroadcastInterval)
|
||||
return;
|
||||
|
||||
villagerBroadcastAccumulator = 0f;
|
||||
if (Villager.villagers == null)
|
||||
return;
|
||||
|
||||
foreach (var villager in Villager.villagers.data)
|
||||
// send batched building placement info
|
||||
/*if (PlaceHook.QueuedBuildings.Count > 0 && (FixedUpdateInterval % 25 == 0))
|
||||
{
|
||||
if (villager == null)
|
||||
continue;
|
||||
|
||||
Guid guid = villager.guid;
|
||||
Component villagerComponent = (Component)(object)villager;
|
||||
if (villagerComponent == null)
|
||||
continue;
|
||||
|
||||
Vector3 currentPosition = villagerComponent.transform.position;
|
||||
if (villagerPositionCache.TryGetValue(guid, out Vector3 lastPosition)
|
||||
&& Vector3.Distance(lastPosition, currentPosition) < VillagerMovementThreshold)
|
||||
foreach (Building building in PlaceHook.QueuedBuildings)
|
||||
{
|
||||
continue;
|
||||
new WorldPlace()
|
||||
{
|
||||
uniqueName = building.UniqueName,
|
||||
customName = building.customName,
|
||||
guid = building.guid,
|
||||
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,
|
||||
life = building.Life,
|
||||
ModifiedMaxLife = building.ModifiedMaxLife,
|
||||
//CollectForBuild = CollectForBuild,
|
||||
yearBuilt = building.YearBuilt,
|
||||
decayProtection = building.decayProtection,
|
||||
seenByPlayer = building.seenByPlayer
|
||||
}.Send();
|
||||
}
|
||||
|
||||
villagerPositionCache[guid] = currentPosition;
|
||||
new VillagerTeleportTo()
|
||||
{
|
||||
guid = guid,
|
||||
pos = currentPosition
|
||||
}.SendToAll();
|
||||
}
|
||||
PlaceHook.QueuedBuildings.Clear();
|
||||
}*/
|
||||
|
||||
FixedUpdateInterval++;
|
||||
}
|
||||
|
||||
#region "TransitionTo"
|
||||
@@ -269,11 +311,22 @@ namespace KCM
|
||||
{
|
||||
try
|
||||
{
|
||||
ServerBrowser.serverBrowserRef.SetActive(state == MenuState.ServerBrowser);
|
||||
ServerBrowser.serverLobbyRef.SetActive(state == MenuState.ServerLobby);
|
||||
// Null checks for ServerBrowser references
|
||||
if (ServerBrowser.serverBrowserRef != null)
|
||||
ServerBrowser.serverBrowserRef.SetActive(state == MenuState.ServerBrowser);
|
||||
|
||||
ServerBrowser.KCMUICanvas.gameObject.SetActive((int)state > 21);
|
||||
helper.Log(((int)state > 21).ToString());
|
||||
if (ServerBrowser.serverLobbyRef != null)
|
||||
ServerBrowser.serverLobbyRef.SetActive(state == MenuState.ServerLobby);
|
||||
|
||||
if (ServerBrowser.KCMUICanvas != null)
|
||||
{
|
||||
ServerBrowser.KCMUICanvas.gameObject.SetActive((int)state > 21);
|
||||
if (state == MenuState.ServerBrowser)
|
||||
{
|
||||
Main.helper?.Log($"TransitionTo ServerBrowser: browserRef={(ServerBrowser.serverBrowserRef != null ? "ready" : "null")}, canvas={(ServerBrowser.KCMUICanvas != null ? "ready" : "null")}");
|
||||
}
|
||||
helper.Log(((int)state > 21).ToString());
|
||||
}
|
||||
|
||||
GameState.inst.mainMenuMode.TransitionTo((MainMenuMode.State)state);
|
||||
}
|
||||
@@ -303,9 +356,6 @@ namespace KCM
|
||||
helper.Log("Preload start in main");
|
||||
try
|
||||
{
|
||||
|
||||
|
||||
//MainMenuPatches.Patch();
|
||||
Main.helper = helper;
|
||||
helper.Log(helper.modPath);
|
||||
|
||||
@@ -440,7 +490,6 @@ namespace KCM
|
||||
// Your code here
|
||||
|
||||
// Get the name of the last method that called OnPlayerPlacement
|
||||
string callTree = "";
|
||||
List<string> strings = new List<string>();
|
||||
|
||||
for (int i = 1; i < 10; i++)
|
||||
@@ -716,17 +765,17 @@ namespace KCM
|
||||
if (KCClient.client.IsConnected)
|
||||
{
|
||||
LogStep(true);
|
||||
__instance.Buildings.Add(b);
|
||||
Component[] storages = ResourceStorageHelper.GetStorages(b);
|
||||
LogStep();
|
||||
for (int i = 0; i < storages.Length; i++)
|
||||
{
|
||||
bool flag = !ResourceStorageHelper.IsPrivate(storages[i]);
|
||||
if (flag)
|
||||
__instance.Buildings.Add(b);
|
||||
IResourceStorage[] storages = b.GetComponents<IResourceStorage>();
|
||||
LogStep();
|
||||
for (int i = 0; i < storages.Length; i++)
|
||||
{
|
||||
ResourceStorageHelper.Register(storages[i]);
|
||||
bool flag = !storages[i].IsPrivate();
|
||||
if (flag)
|
||||
{
|
||||
FreeResourceManager.inst.AddResourceStorage(storages[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
LogStep();
|
||||
int landMass = b.LandMass();
|
||||
Home res = b.GetComponent<Home>();
|
||||
@@ -772,8 +821,6 @@ namespace KCM
|
||||
}
|
||||
LogStep();
|
||||
|
||||
// CRITICAL: Bake pathing for villager movement!
|
||||
b.BakePathing();
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -827,7 +874,6 @@ namespace KCM
|
||||
new AddVillagerPacket()
|
||||
{
|
||||
guid = __result.guid,
|
||||
position = pos, // Include villager spawn position
|
||||
}.Send();
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -1218,14 +1264,18 @@ namespace KCM
|
||||
public static bool Prefix(ref string __result)
|
||||
{
|
||||
Main.helper.Log("Get save dir");
|
||||
if (KCServer.IsRunning || KCClient.client.IsConnected)
|
||||
if (KCClient.client.IsConnected)
|
||||
{
|
||||
if (KCServer.IsRunning)
|
||||
{
|
||||
|
||||
}
|
||||
__result = Application.persistentDataPath + "/Saves/Multiplayer";
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
__result = Application.persistentDataPath + "/Saves";
|
||||
__result = Application.persistentDataPath + "/Saves"; ;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -1254,22 +1304,8 @@ namespace KCM
|
||||
Stream file = new FileStream(path, FileMode.Open);
|
||||
try
|
||||
{
|
||||
object deserialized = bf.Deserialize(file);
|
||||
|
||||
if (deserialized is MultiplayerSaveContainer loadData)
|
||||
{
|
||||
loadData.Unpack(null);
|
||||
}
|
||||
else if (deserialized is LoadSaveContainer legacyLoadData)
|
||||
{
|
||||
Main.helper.Log($"Deserialized fallback save type ({legacyLoadData.GetType().FullName}), applying base container.");
|
||||
legacyLoadData.Unpack(null);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new InvalidCastException($"Unexpected save data type: {deserialized?.GetType().FullName}");
|
||||
}
|
||||
|
||||
MultiplayerSaveContainer loadData = (MultiplayerSaveContainer)bf.Deserialize(file);
|
||||
loadData.Unpack(null);
|
||||
Broadcast.OnLoadedEvent.Broadcast(new OnLoadedEvent());
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -1313,14 +1349,35 @@ namespace KCM
|
||||
{
|
||||
Main.helper.Log("Attempting to load save from server");
|
||||
|
||||
using (MemoryStream ms = new MemoryStream(saveBytes))
|
||||
try
|
||||
{
|
||||
BinaryFormatter bf = new BinaryFormatter();
|
||||
bf.Binder = new MultiplayerSaveDeserializationBinder();
|
||||
saveContainer = (MultiplayerSaveContainer)bf.Deserialize(ms);
|
||||
using (MemoryStream ms = new MemoryStream(saveBytes))
|
||||
{
|
||||
BinaryFormatter bf = new BinaryFormatter();
|
||||
bf.Binder = new MultiplayerSaveDeserializationBinder();
|
||||
saveContainer = (MultiplayerSaveContainer)bf.Deserialize(ms);
|
||||
}
|
||||
|
||||
Main.helper.Log("Deserialize complete, calling Unpack...");
|
||||
saveContainer.Unpack(null);
|
||||
Main.helper.Log("Unpack complete!");
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Main.helper.Log("Error loading save from server");
|
||||
Main.helper.Log(e.Message);
|
||||
Main.helper.Log(e.StackTrace);
|
||||
if (e.InnerException != null)
|
||||
{
|
||||
Main.helper.Log("Inner exception: " + e.InnerException.Message);
|
||||
Main.helper.Log(e.InnerException.StackTrace);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
memoryStreamHook = false;
|
||||
}
|
||||
|
||||
memoryStreamHook = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1475,55 +1532,34 @@ namespace KCM
|
||||
{
|
||||
if (KCClient.client.IsConnected)
|
||||
{
|
||||
Main.helper.Log($"[ProcessBuilding] START - {structureData.uniqueName}");
|
||||
|
||||
try
|
||||
Building Building = GameState.inst.GetPlaceableByUniqueName(structureData.uniqueName);
|
||||
bool flag = Building;
|
||||
if (flag)
|
||||
{
|
||||
Building Building = GameState.inst.GetPlaceableByUniqueName(structureData.uniqueName);
|
||||
Main.helper.Log($"[ProcessBuilding] GetPlaceable: {(Building != null ? "OK" : "NULL")}");
|
||||
Building building = UnityEngine.Object.Instantiate<Building>(Building);
|
||||
building.transform.position = structureData.globalPosition;
|
||||
building.Init();
|
||||
building.transform.SetParent(p.buildingContainer.transform, true);
|
||||
structureData.Unpack(building);
|
||||
p.AddBuilding(building);
|
||||
|
||||
if (Building)
|
||||
Main.helper.Log($"Loading player id: {p.PlayerLandmassOwner.teamId}");
|
||||
Main.helper.Log($"loading building: {building.FriendlyName}");
|
||||
Main.helper.Log($" (teamid: {building.TeamID()})");
|
||||
Main.helper.Log(p.ToString());
|
||||
bool flag2 = building.GetComponent<Keep>() != null && building.TeamID() == p.PlayerLandmassOwner.teamId;
|
||||
Main.helper.Log("Set keep? " + flag2);
|
||||
if (flag2)
|
||||
{
|
||||
Building building = UnityEngine.Object.Instantiate<Building>(Building);
|
||||
Main.helper.Log($"[ProcessBuilding] Instantiated");
|
||||
|
||||
building.transform.position = structureData.globalPosition;
|
||||
Main.helper.Log($"[ProcessBuilding] Position set: {structureData.globalPosition}");
|
||||
|
||||
building.Init();
|
||||
Main.helper.Log($"[ProcessBuilding] Init done");
|
||||
|
||||
building.transform.SetParent(p.buildingContainer.transform, true);
|
||||
Main.helper.Log($"[ProcessBuilding] SetParent done");
|
||||
|
||||
structureData.Unpack(building);
|
||||
Main.helper.Log($"[ProcessBuilding] Unpack done");
|
||||
|
||||
// NOTE: AddBuilding, PlaceFromLoad and UnpackStage2 are called by the original
|
||||
// PlayerSaveData.Unpack method after ProcessBuilding returns
|
||||
Main.helper.Log($"[ProcessBuilding] Returning building to Unpack");
|
||||
|
||||
bool isKeep = building.GetComponent<Keep>() != null && building.TeamID() == p.PlayerLandmassOwner.teamId;
|
||||
if (isKeep)
|
||||
{
|
||||
p.keep = building.GetComponent<Keep>();
|
||||
Main.helper.Log($"[ProcessBuilding] Keep set");
|
||||
}
|
||||
|
||||
__result = building;
|
||||
Main.helper.Log($"[ProcessBuilding] SUCCESS - {structureData.uniqueName}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Main.helper.Log($"[ProcessBuilding] FAILED - {structureData.uniqueName} not found in GameState");
|
||||
__result = null;
|
||||
p.keep = building.GetComponent<Keep>();
|
||||
Main.helper.Log(p.keep.ToString());
|
||||
}
|
||||
__result = building;
|
||||
}
|
||||
catch (Exception e)
|
||||
else
|
||||
{
|
||||
Main.helper.Log($"[ProcessBuilding] EXCEPTION: {structureData.uniqueName}");
|
||||
Main.helper.Log($"[ProcessBuilding] Error: {e.Message}");
|
||||
Main.helper.Log($"[ProcessBuilding] Stack: {e.StackTrace}");
|
||||
Main.helper.Log(structureData.uniqueName + " failed to load correctly");
|
||||
__result = null;
|
||||
}
|
||||
|
||||
@@ -1700,9 +1736,10 @@ namespace KCM
|
||||
__instance.JobCustomMaxEnabledFlag = new bool[World.inst.NumLandMasses][];
|
||||
for (int lm = 0; lm < World.inst.NumLandMasses; lm++)
|
||||
{
|
||||
__instance.JobFilledAvailable[lm] = new int[38];
|
||||
__instance.JobCustomMaxEnabledFlag[lm] = new bool[38];
|
||||
for (int n = 0; n < 38; n++)
|
||||
int numJobTypes = p.JobFilledAvailable.data[lm].GetLength(0);
|
||||
__instance.JobFilledAvailable[lm] = new int[numJobTypes];
|
||||
__instance.JobCustomMaxEnabledFlag[lm] = new bool[numJobTypes];
|
||||
for (int n = 0; n < numJobTypes; n++)
|
||||
{
|
||||
__instance.JobFilledAvailable[lm][n] = p.JobFilledAvailable.data[lm][n, 1];
|
||||
}
|
||||
|
||||
91
Packets/Game/GameBuilding/BuildingRemovePacket.cs
Normal file
91
Packets/Game/GameBuilding/BuildingRemovePacket.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ namespace KCM.Packets.Game.GamePlayer
|
||||
public override ushort packetId => (ushort)Enums.Packets.AddVillager;
|
||||
|
||||
public Guid guid { get; set; }
|
||||
public Vector3 position { get; set; }
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
@@ -20,33 +19,18 @@ namespace KCM.Packets.Game.GamePlayer
|
||||
{
|
||||
if (KCClient.client.Id == clientId) return;
|
||||
|
||||
// Check for duplicate villager by guid
|
||||
var existingVillager = player.inst.Workers.data.FirstOrDefault(w => w != null && w.guid == guid);
|
||||
if (existingVillager != null)
|
||||
{
|
||||
Main.helper.Log($"Villager with guid {guid} already exists, skipping duplicate");
|
||||
return;
|
||||
}
|
||||
|
||||
Main.helper.Log("Received add villager packet from " + player.name + $"({player.id})");
|
||||
|
||||
Villager newVillager = Villager.CreateVillager();
|
||||
newVillager.guid = guid;
|
||||
Villager v = Villager.CreateVillager();
|
||||
v.guid = guid;
|
||||
|
||||
// Set villager position
|
||||
if (position != Vector3.zero)
|
||||
{
|
||||
newVillager.TeleportTo(position);
|
||||
}
|
||||
|
||||
player.inst.Workers.Add(newVillager);
|
||||
player.inst.Homeless.Add(newVillager);
|
||||
player.inst.Workers.Add(v);
|
||||
player.inst.Homeless.Add(v);
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Main.helper.Log("Error handling add villager packet: " + e.Message);
|
||||
Main.helper.Log(e.StackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,24 +46,7 @@ namespace KCM.Packets.Game.GameWorld
|
||||
|
||||
public void PlaceBuilding()
|
||||
{
|
||||
Main.LogSync("========== BUILDING PLACEMENT START ==========");
|
||||
Main.LogSync($"Building: {uniqueName} from player: {player?.name} (id={player?.id})");
|
||||
Main.LogSync($" guid={guid}");
|
||||
Main.LogSync($" globalPosition={globalPosition}");
|
||||
Main.LogSync($" localPosition={localPosition}");
|
||||
Main.LogSync($" rotation={rotation} (euler={rotation.eulerAngles})");
|
||||
Main.LogSync($" built={built}, placed={placed}, open={open}");
|
||||
Main.LogSync($" constructionProgress={constructionProgress}, constructionPaused={constructionPaused}");
|
||||
Main.LogSync($" life={life}, ModifiedMaxLife={ModifiedMaxLife}");
|
||||
Main.LogSync($" yearBuilt={yearBuilt}, decayProtection={decayProtection}");
|
||||
|
||||
// Check for duplicate building by guid to prevent double placement from network retries
|
||||
var existingBuilding = player.inst.Buildings.data.FirstOrDefault(b => b != null && b.guid == guid);
|
||||
if (existingBuilding != null)
|
||||
{
|
||||
Main.helper.Log($"Building with guid {guid} already exists for player {player.name}, skipping duplicate placement");
|
||||
return;
|
||||
}
|
||||
Main.helper.Log("Received place building packet for " + uniqueName + " from " + player.name + $"({player.id})");
|
||||
|
||||
//var originalPlayer = Player.inst;
|
||||
//Player.inst = player.inst;
|
||||
@@ -111,7 +94,6 @@ namespace KCM.Packets.Game.GameWorld
|
||||
Main.helper.Log(building.LandMass().ToString());
|
||||
Main.helper.Log("Player add Building unpacked");
|
||||
player.inst.AddBuilding(building);
|
||||
Main.ApplyPendingBuildingState(building);
|
||||
|
||||
try
|
||||
{
|
||||
@@ -134,23 +116,6 @@ namespace KCM.Packets.Game.GameWorld
|
||||
Main.helper.Log("unpack stage 2");
|
||||
structureData.UnpackStage2(building);
|
||||
|
||||
// Update materials/textures for correct display
|
||||
building.UpdateMaterialSelection();
|
||||
|
||||
// Update road rotation for proper visuals
|
||||
Road roadComp = building.GetComponent<Road>();
|
||||
if (roadComp != null)
|
||||
{
|
||||
roadComp.UpdateRotation();
|
||||
}
|
||||
|
||||
// Update aqueduct rotation
|
||||
Aqueduct aqueductComp = building.GetComponent<Aqueduct>();
|
||||
if (aqueductComp != null)
|
||||
{
|
||||
aqueductComp.UpdateRotation();
|
||||
}
|
||||
|
||||
building.SetVisibleForFog(false);
|
||||
|
||||
Main.helper.Log("Landmass owner take ownership");
|
||||
@@ -165,24 +130,15 @@ namespace KCM.Packets.Game.GameWorld
|
||||
Main.helper.Log($"Client player ({player.name}) Landmass Names Count: {player.inst.LandMassNames.Count}, Contents: {string.Join(", ", player.inst.LandMassNames)}");
|
||||
|
||||
player.inst.LandMassNames[building.LandMass()] = player.kingdomName;
|
||||
Player.inst.LandMassNames[building.LandMass()] = player.kingdomName;
|
||||
Player.inst.LandMassNames[building.LandMass()] = player.kingdomName;
|
||||
|
||||
// Log final building state after placement
|
||||
Main.LogSync("---------- BUILDING PLACED FINAL STATE ----------");
|
||||
Main.LogSync($" Final position: {building.transform.position}");
|
||||
if (building.transform.childCount > 0)
|
||||
{
|
||||
Main.LogSync($" Child[0] rotation: {building.transform.GetChild(0).rotation} (euler={building.transform.GetChild(0).rotation.eulerAngles})");
|
||||
Main.LogSync($" Child[0] localPosition: {building.transform.GetChild(0).localPosition}");
|
||||
}
|
||||
Main.LogSync($" IsBuilt={building.IsBuilt()}, IsPlaced={building.IsPlaced()}");
|
||||
Main.LogSync($" TeamID={building.TeamID()}, LandMass={building.LandMass()}");
|
||||
Main.LogSync("========== BUILDING PLACEMENT END ==========");
|
||||
//Player.inst = originalPlayer;
|
||||
}
|
||||
else
|
||||
{
|
||||
Main.LogSync($"FAILED to place building: {structureData.uniqueName} - GetPlaceableByUniqueName returned null");
|
||||
Main.helper.Log(structureData.uniqueName + " failed to load correctly");
|
||||
}
|
||||
//building.Init();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -128,7 +128,8 @@ namespace KCM.Packets.Handlers
|
||||
|
||||
IPacket packet = DeserialisePacket(messageReceived);
|
||||
|
||||
Main.LogSync($"SERVER RECV: {Packets[id].packet.GetType().Name} (id={id}) from client {messageReceived.FromConnection.Id}");
|
||||
//Main.helper.Log($"Server Received packet {Packets[id].packet.GetType().Name} from {messageReceived.FromConnection.Id}");
|
||||
|
||||
|
||||
if (KCServer.IsRunning)
|
||||
{
|
||||
@@ -168,9 +169,11 @@ namespace KCM.Packets.Handlers
|
||||
var id = messageReceived.MessageId;
|
||||
|
||||
|
||||
//Main.helper.Log($"Client Received packet {Packets[id].packet.GetType().Name} from {messageReceived.FromConnection.Id}");
|
||||
|
||||
IPacket packet = DeserialisePacket(messageReceived);
|
||||
|
||||
Main.LogSync($"CLIENT RECV: {Packets[id].packet.GetType().Name} (id={id}) from client {packet.clientId}");
|
||||
//Main.helper.Log($"Client Received packet {Packets[id].packet.GetType().Name} from {packet.clientId}");
|
||||
|
||||
if (KCClient.client.IsConnected)
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Riptide.Demos.Steam.PlayerHosted;
|
||||
using static KCM.Main;
|
||||
|
||||
namespace KCM.Packets.Lobby
|
||||
@@ -29,84 +30,90 @@ namespace KCM.Packets.Lobby
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
float savePercent = (float)received / (float)saveSize;
|
||||
|
||||
// Initialize saveData and chunksReceived on the first packet received
|
||||
if (saveData.Length == 1)
|
||||
// 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;
|
||||
|
||||
ServerLobbyScript.LoadingSave.SetActive(true);
|
||||
|
||||
// save percentage
|
||||
|
||||
|
||||
saveData = new byte[saveSize];
|
||||
chunksReceived = new bool[totalChunks];
|
||||
received = 0;
|
||||
|
||||
ServerLobbyScript.LoadingSave.SetActive(true);
|
||||
}
|
||||
|
||||
// Skip if we already received this chunk (duplicate packet)
|
||||
if (chunksReceived[chunkId])
|
||||
{
|
||||
Main.helper.Log($"[SaveTransfer] Duplicate chunk {chunkId} received, skipping.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Copy the chunk data into the correct position in saveData
|
||||
Array.Copy(saveDataChunk, 0, saveData, saveDataIndex, saveDataChunk.Length);
|
||||
|
||||
// Mark this chunk as received
|
||||
chunksReceived[chunkId] = true;
|
||||
|
||||
// Seek to the next position to write to
|
||||
received += chunkSize;
|
||||
|
||||
Main.helper.Log($"[SaveTransfer] Processed chunk {chunkId}/{totalChunks}. Received: {received} bytes of {saveSize}.");
|
||||
|
||||
ServerLobbyScript.ProgressBar.fillAmount = savePercent;
|
||||
ServerLobbyScript.ProgressBarText.text = (savePercent * 100).ToString("0.00") + "%";
|
||||
ServerLobbyScript.ProgressText.text = $"{((float)(received / 1000)).ToString("0.00")} KB / {((float)(saveSize / 1000)).ToString("0.00")} KB";
|
||||
|
||||
|
||||
if (chunkId + 1 == totalChunks)
|
||||
// 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();
|
||||
|
||||
GameState.inst.SetNewMode(GameState.inst.playingMode);
|
||||
LobbyManager.loadingSave = false;
|
||||
|
||||
LoadSaveLoadHook.saveContainer.Unpack(null);
|
||||
Broadcast.OnLoadedEvent.Broadcast(new OnLoadedEvent());
|
||||
|
||||
ServerLobbyScript.LoadingSave.SetActive(false);
|
||||
|
||||
// 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()
|
||||
{
|
||||
return chunksReceived.All(x => x == true);
|
||||
}
|
||||
|
||||
public static string WhichIsNotComplete()
|
||||
{
|
||||
string notComplete = "";
|
||||
for (int i = 0; i < chunksReceived.Length; i++)
|
||||
{
|
||||
if (!chunksReceived[i])
|
||||
{
|
||||
notComplete += i + ", ";
|
||||
}
|
||||
}
|
||||
return notComplete;
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
}
|
||||
|
||||
@@ -18,38 +18,26 @@ 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
|
||||
{
|
||||
if (!LobbyManager.loadingSave)
|
||||
{
|
||||
SpeedControlUI.inst.SetSpeed(0);
|
||||
SpeedControlUI.inst.SetSpeed(0);
|
||||
|
||||
try
|
||||
{
|
||||
typeof(MainMenuMode).GetMethod("StartGame", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(GameState.inst.mainMenuMode, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log(ex.Message.ToString());
|
||||
Main.helper.Log(ex.ToString());
|
||||
}
|
||||
|
||||
SpeedControlUI.inst.SetSpeed(0);
|
||||
}
|
||||
else
|
||||
try
|
||||
{
|
||||
LobbyManager.loadingSave = false;
|
||||
GameState.inst.SetNewMode(GameState.inst.playingMode);
|
||||
typeof(MainMenuMode).GetMethod("StartGame", BindingFlags.NonPublic | BindingFlags.Instance).Invoke(GameState.inst.mainMenuMode, null);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Main.helper.Log(ex.Message.ToString());
|
||||
Main.helper.Log(ex.ToString());
|
||||
}
|
||||
|
||||
SpeedControlUI.inst.SetSpeed(0);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Handle exception here
|
||||
Main.helper.Log(ex.Message.ToString());
|
||||
Main.helper.Log(ex.ToString());
|
||||
}
|
||||
@@ -57,47 +45,18 @@ namespace KCM.Packets.Lobby
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
Start();
|
||||
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();
|
||||
}*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -99,10 +99,7 @@ namespace KCM.Packets.Network
|
||||
chunkSize = chunk.Length,
|
||||
saveDataIndex = sent,
|
||||
totalChunks = chunks.Count
|
||||
}.Send(clientId);
|
||||
|
||||
Main.helper.Log(" ");
|
||||
|
||||
}.SendReliable(clientId);
|
||||
packetsSent++;
|
||||
sent += chunk.Length;
|
||||
}
|
||||
|
||||
@@ -33,21 +33,12 @@ namespace KCM.Packets.Network
|
||||
CloudSystem.inst.BaseFreq = 4.5f;
|
||||
Weather.inst.SetSeason(Weather.Season.Summer);
|
||||
|
||||
//inst = new KCClient(KCServer.IsRunning ? "Ryan" : "Orion");
|
||||
KCClient.inst = new KCClient(SteamFriends.GetPersonaName());
|
||||
|
||||
Main.helper.Log("Sending client connected. Client ID is: " + clientId);
|
||||
|
||||
KCPlayer localPlayer;
|
||||
if (!Main.kCPlayers.TryGetValue(Main.PlayerSteamID, out localPlayer))
|
||||
{
|
||||
localPlayer = new KCPlayer(KCClient.inst.Name, clientId, Main.PlayerSteamID);
|
||||
Main.kCPlayers.Add(Main.PlayerSteamID, localPlayer);
|
||||
}
|
||||
else
|
||||
{
|
||||
localPlayer.id = clientId;
|
||||
localPlayer.name = KCClient.inst.Name;
|
||||
}
|
||||
Main.kCPlayers.Add(Main.PlayerSteamID, new KCPlayer(KCClient.inst.Name, clientId, Main.PlayerSteamID));
|
||||
|
||||
Player.inst.PlayerLandmassOwner.teamId = clientId * 10 + 2;
|
||||
|
||||
|
||||
@@ -20,9 +20,6 @@ namespace KCM.Packets
|
||||
|
||||
if (!Main.clientSteamIds.ContainsKey(clientId))
|
||||
return null;
|
||||
|
||||
//Main.helper.Log($"SteamID: {Main.GetPlayerByClientID(clientId).steamId} for {clientId} ({Main.GetPlayerByClientID(clientId).id})");
|
||||
|
||||
if (Main.kCPlayers.TryGetValue(Main.GetPlayerByClientID(clientId).steamId, out p))
|
||||
return p;
|
||||
else
|
||||
@@ -38,7 +35,6 @@ namespace KCM.Packets
|
||||
{
|
||||
try
|
||||
{
|
||||
Main.LogSync($"SEND TO ALL: {this.GetType().Name} (id={packetId}) except={exceptToClient}");
|
||||
if (exceptToClient == 0)
|
||||
{
|
||||
if (KCServer.IsRunning)
|
||||
@@ -76,7 +72,6 @@ namespace KCM.Packets
|
||||
{
|
||||
try
|
||||
{
|
||||
Main.LogSync($"SEND: {this.GetType().Name} (id={packetId}) to={toClient} myId={KCClient.client?.Id}");
|
||||
if (KCClient.client.IsConnected && toClient == 0)
|
||||
{
|
||||
this.clientId = KCClient.client.Id;
|
||||
@@ -109,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();
|
||||
}
|
||||
|
||||
@@ -36,41 +36,25 @@ namespace KCM.Packets.State
|
||||
{
|
||||
if (clientId == KCClient.client.Id) return; //prevent double placing on same client
|
||||
|
||||
//Main.helper.Log("Received building state packet for: " + uniqueName + " from " + Main.kCPlayers[Main.GetPlayerByClientID(clientId).steamId].name + $"({clientId})");
|
||||
|
||||
|
||||
Building building = player.inst.GetBuilding(guid);
|
||||
|
||||
if (building == null)
|
||||
{
|
||||
Main.helper.Log("Building not found.");
|
||||
Main.QueuePendingBuildingState(this);
|
||||
return;
|
||||
}
|
||||
|
||||
ApplyToBuilding(building);
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
//throw new NotImplementedException();
|
||||
}
|
||||
|
||||
public void ApplyToBuilding(Building building)
|
||||
{
|
||||
if (building == null)
|
||||
return;
|
||||
|
||||
Main.LogSync($"========== BUILDING STATE UPDATE ==========");
|
||||
Main.LogSync($"Building: {uniqueName} guid={guid}");
|
||||
Main.LogSync($" globalPosition={globalPosition}");
|
||||
Main.LogSync($" localPosition={localPosition}");
|
||||
Main.LogSync($" rotation={rotation} (euler={rotation.eulerAngles})");
|
||||
Main.LogSync($" built={built}, placed={placed}, open={open}");
|
||||
Main.LogSync($" constructionProgress={constructionProgress}");
|
||||
|
||||
try
|
||||
{
|
||||
//PrintProperties();
|
||||
|
||||
building.UniqueName = uniqueName;
|
||||
building.customName = customName;
|
||||
|
||||
|
||||
building.transform.position = this.globalPosition;
|
||||
building.transform.GetChild(0).rotation = this.rotation;
|
||||
building.transform.GetChild(0).localPosition = this.localPosition;
|
||||
@@ -79,6 +63,7 @@ namespace KCM.Packets.State
|
||||
SetPrivateFieldValue(building, "placed", placed);
|
||||
SetPrivateFieldValue(building, "resourceProgress", resourceProgress);
|
||||
|
||||
|
||||
building.Open = open;
|
||||
building.doBuildAnimation = doBuildAnimation;
|
||||
building.constructionPaused = constructionPaused;
|
||||
@@ -86,6 +71,7 @@ namespace KCM.Packets.State
|
||||
building.Life = life;
|
||||
building.ModifiedMaxLife = ModifiedMaxLife;
|
||||
|
||||
|
||||
//building.yearBuilt = yearBuilt;
|
||||
SetPrivateFieldValue(building, "yearBuilt", yearBuilt);
|
||||
|
||||
@@ -100,6 +86,11 @@ namespace KCM.Packets.State
|
||||
}
|
||||
}
|
||||
|
||||
public override void HandlePacketServer()
|
||||
{
|
||||
//throw new NotImplementedException();
|
||||
}
|
||||
|
||||
private void SetPrivateFieldValue(object obj, string fieldName, object value)
|
||||
{
|
||||
Type type = obj.GetType();
|
||||
|
||||
299
README.md
299
README.md
@@ -1,283 +1,38 @@
|
||||
# Kingdoms and Castles Multiplayer Mod - Bug Tracker
|
||||
# Kingdoms and Castles Multiplayer Mod Fixes
|
||||
|
||||
## Ismert hibak / Known Issues
|
||||
This document summarizes the fixes and improvements implemented to enhance the stability and functionality of the multiplayer mod for Kingdoms and Castles.
|
||||
|
||||
### KRITIKUS - Server/Connection Problems
|
||||
## Implemented Fixes:
|
||||
|
||||
| Hiba | Status | Megjegyzes |
|
||||
|------|--------|------------|
|
||||
| Server nem all le amikor host kilep menube | Meg nincs elkezdve | Host menu-be megy, de server tovabb fut es fogadja a packeteket a klienstol |
|
||||
| Kliens nem lesz kidobva host kilepesekor | Meg nincs elkezdve | Kliens tovabb jatszik miutan host kiment, nem kap ertesitest |
|
||||
| Packetek erkeznek menu-ben | Meg nincs elkezdve | Menu-ben is fogadja es feldolgozza a packeteket, ami nem helyes |
|
||||
| Rossz kapcsolat, "server disconnected" hibak | Reszben javitva | Event handler duplikacio es session cleanup javitva |
|
||||
| StartGame.Start() NullReferenceException | Meg nincs elkezdve | MainMenuMode.StartGame crash-el 2x a logban, TargetInvocationException |
|
||||
### 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.
|
||||
|
||||
### Building Placement Errors (output.txt-bol) - KRITIKUS!
|
||||
### 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.
|
||||
|
||||
| Hiba | Status | Megjegyzes |
|
||||
|------|--------|------------|
|
||||
| **PlayerAddBuildingHook NullReferenceException** | Meg nincs elkezdve | **56 PLACEMENT START, csak 1 PLACEMENT END!** Szinte minden epulet fail-el. A hook 1-11 szamokat printeli majd crash |
|
||||
| IndexOutOfRangeException WorldPlace-ben | Meg nincs elkezdve | 9 elofordulas - "Index was outside the bounds of the array" |
|
||||
| Epuletek nem jelennek meg kliensnel | Vizsgalat alatt | A fenti hibak miatt 55/56 epulet NEM kerul elhelyezesre! |
|
||||
### 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.
|
||||
|
||||
### Host-Client Sync Problems
|
||||
### 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).
|
||||
|
||||
| Hiba | Status | Megjegyzes |
|
||||
|------|--------|------------|
|
||||
| Jatek ujrainditasa szukseges lobby/save valtas utan | Reszben javitva | Session cleanup hozzaadva LobbyManager-ben |
|
||||
| Utak/epuletek nem toltenek be helyesen vagy atfednek | Vizsgalat alatt | UpdateMaterialSelection() es UpdateRotation() hozzaadva |
|
||||
| Eroforrasok nem mentenek/toltenek helyesen | Meg nincs elkezdve | |
|
||||
| NPC-k veletlenszeruen megallnak es nem mozognak load utan | Vizsgalat alatt | TeleportTo problema javitva, BakePathing hozzaadva |
|
||||
| Orientaciok (rotaciok) nem szinkronizalodnak | Reszben javitva | Rotation es localPosition kozvetlenul alkalmazva WorldPlace-ben |
|
||||
| Host torol valamit -> kliens nem latja | Meg nincs elkezdve | BuildingDestroy packet szukseges |
|
||||
| Host nem latja a kliens epuleteit helyesen (rossz texturak) | Javitva | UpdateMaterialSelection() hozzaadva WorldPlace.cs-ben |
|
||||
### 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.
|
||||
|
||||
### Gameplay Bugs
|
||||
### 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.
|
||||
|
||||
| Hiba | Status | Megjegyzes |
|
||||
|------|--------|------------|
|
||||
| Tobb Keep ugyanarra a szigetre | Meg nincs elkezdve | Engedi hogy masik jatekos szigetere Keep-et rakjunk, igy az eredeti jatekos elveszti a Keep-jet es nem tudja mozgatni az embereit |
|
||||
### 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.
|
||||
|
||||
### Status Definiciok
|
||||
## Pending Task:
|
||||
|
||||
- **Javitva**: A hiba javitva lett es tesztelve
|
||||
- **Reszben javitva**: Javitas megkezdve, de meg nem teljes
|
||||
- **Vizsgalat alatt**: Debug logging hozzaadva, vizsgaljuk a problemat
|
||||
- **Meg nincs elkezdve**: A hiba ismert, de meg nem kezdtuk el javitani
|
||||
|
||||
## Log Analisis (2024-12-14 15:39-15:56)
|
||||
|
||||
### Hibak szamokban
|
||||
- **56 BUILDING PLACEMENT START** - epulet elhelyezesi kiserlet
|
||||
- **1 BUILDING PLACEMENT END** - sikeres elhelyezes
|
||||
- **55 FAIL (98%)** - majdnem minden epulet elbukik!
|
||||
- **55 "Error in add building hook"** - NullReferenceException
|
||||
- **9 IndexOutOfRangeException** - tomb tulindexeles
|
||||
- **2 StartGame.Start() crash** - TargetInvocationException
|
||||
- **2 Client disconnect event** - kapcsolat megszakadas
|
||||
|
||||
### Idovonal
|
||||
- 15:39: Session start
|
||||
- 15:40:03: Exception MainMenuMode.StartGame - NullReferenceException (1.)
|
||||
- 15:42:14: Client disconnect events (ketszer)
|
||||
- 15:44:11: Ujabb Exception MainMenuMode.StartGame (2.)
|
||||
- 15:45-15:50: **55x "Error in add building hook"** - szinte minden epulet fail
|
||||
- 15:52:00: Utolso BuildingStatePacket erkezik client 2-tol, utana csak ServerSettings
|
||||
- 15:53:55: **Host kilep menube** ("Menu set to: Menu")
|
||||
- 15:53:55-15:56:25: **Server TOVABB FUT** es fogadja a packeteket a klienstol!
|
||||
- 15:56:27: Server vegre leall
|
||||
|
||||
### Fo problemak a logbol
|
||||
1. **PlayerAddBuildingHook.Prefix** crash-el 55x - valami null benne (1-11 szamokat printel elotte)
|
||||
2. **Server nem all le menu-nel** - 2.5 percig meg fut miutan host kiment
|
||||
3. **Kliens nem kap ertesitest** - tovabb kuldi a packeteket
|
||||
4. **StartGame exception** - 2x crash jatek inditaskor
|
||||
|
||||
## Recent Changes
|
||||
|
||||
### 2024-12-14
|
||||
- Advanced sync logging hozzaadva `[SYNC]` prefix-szel
|
||||
- Building placement reszletes logging (minden property)
|
||||
- Packet send/receive logging
|
||||
- Building state update logging
|
||||
|
||||
### Korabbi javitasok
|
||||
- KCServer.cs: Event handler duplikacio javitas
|
||||
- LobbyManager.cs: Session cleanup (clientSteamIds, loadingSave)
|
||||
- WorldPlace.cs: Building guid duplikacio check, rotation/localPosition fix
|
||||
- AddVillagerPacket.cs: Villager position sync + duplikacio check
|
||||
- Main.cs: BakePathing() hozzaadva PlayerAddBuildingHook-ban
|
||||
|
||||
---
|
||||
|
||||
## Kód Hibák Részletes Dokumentációja
|
||||
|
||||
> **FIGYELEM**: Ez a szekció a kódban található hibák részletes elemzését tartalmazza.
|
||||
> A hibák **NEM** lettek javítva, csak dokumentálva vannak a pontos hellyel és javítási javaslatokkal.
|
||||
|
||||
### 1. KRITIKUS: Server nem áll le amikor host kilép menübe
|
||||
|
||||
**Fájl**: `KCServer.cs`
|
||||
**Hiba helye**: Hiányzik a logika - nincs kód ami leállítaná a servert menüváltáskor
|
||||
**Kapcsolódó kód**:
|
||||
- `KCServer.cs:110-122` - `OnApplicationQuit()` metódus (csak alkalmazás bezáráskor hívódik)
|
||||
- `Main.cs:334-346` - `TransitionToHook` (detektálja a menü változást, de nem reagál rá)
|
||||
|
||||
**Probléma részletesen**:
|
||||
```csharp
|
||||
// KCServer.cs:110-122
|
||||
private void OnApplicationQuit()
|
||||
{
|
||||
if (server != null && server.IsRunning)
|
||||
{
|
||||
new ShowModal { ... }.SendToAll();
|
||||
server.Stop();
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- A server CSAK akkor áll le, ha az alkalmazás teljesen bezár (`OnApplicationQuit`)
|
||||
- Amikor a host menübe lép (pl. `MainMenuMode.State.Menu`), a `TransitionToHook` észleli a változást
|
||||
- DE nincs kód ami meghívná a `server.Stop()`-ot
|
||||
- Eredmény: server tovább fut és fogadja a packeteket, kliens nem kap értesítést
|
||||
|
||||
**Hol kell javítani**:
|
||||
1. **Opció A**: `Main.cs:334-346` - TransitionToHook Prefix metódusban
|
||||
- Ellenőrizni kell: ha `newState == MainMenuMode.State.Menu` ÉS `KCServer.IsRunning`
|
||||
- Akkor hívni: `KCServer.server.Stop()` és értesíteni a klienseket
|
||||
|
||||
2. **Opció B**: `KCServer.cs` - új metódus hozzáadása
|
||||
- Létrehozni egy `StopServer()` metódust ami értesíti a klienseket és leállítja a servert
|
||||
- Ezt meghívni a `TransitionToHook`-ból amikor menübe lép a host
|
||||
|
||||
**Miért kritikus**:
|
||||
- Kliens nem tudja, hogy a host kilépett
|
||||
- Server erőforrásokat pazarol
|
||||
- Packetek feldolgozása menüben hibákhoz vezet
|
||||
|
||||
---
|
||||
|
||||
### 2. KRITIKUS: PlayerAddBuildingHook NullReferenceException
|
||||
|
||||
**Fájl**: `Main.cs`
|
||||
**Hiba helye**: `Main.cs:764`
|
||||
**Érintett kód**:
|
||||
```csharp
|
||||
// Main.cs:755-764
|
||||
var globalBuildingRegistry = __instance.GetType().GetField("globalBuildingRegistry", ...).GetValue(__instance) as ArrayExt<Player.BuildingRegistry>;
|
||||
LogStep(); // 7
|
||||
var landMassBuildingRegistry = __instance.GetType().GetField("landMassBuildingRegistry", ...).GetValue(__instance) as ArrayExt<Player.LandMassBuildingRegistry>;
|
||||
LogStep(); // 8
|
||||
var unbuiltBuildingsPerLandmass = __instance.GetType().GetField("unbuiltBuildingsPerLandmass", ...).GetValue(__instance) as ArrayExt<ArrayExt<Building>>;
|
||||
LogStep(); // 9 (utolsó amit elér)
|
||||
|
||||
__instance.AddToRegistry(globalBuildingRegistry, b);
|
||||
LogStep(); // 10 (sosem éri el)
|
||||
__instance.AddToRegistry(landMassBuildingRegistry.data[landMass].registry, b); // <-- CRASH ITT (line 764)
|
||||
```
|
||||
|
||||
**Probléma részletesen**:
|
||||
- A README szerint: **56 PLACEMENT START, csak 1 PLACEMENT END** → 55/56 épület fail
|
||||
- LogStep() 1-11-ig printeli (de legtöbbször csak 1-9-ig jut el)
|
||||
- A crash valószínűleg itt: `landMassBuildingRegistry.data[landMass].registry`
|
||||
- **Lehetséges okok**:
|
||||
1. `landMassBuildingRegistry` null
|
||||
2. `landMassBuildingRegistry.data` null
|
||||
3. `landMassBuildingRegistry.data[landMass]` null (IndexOutOfRange)
|
||||
4. `landMassBuildingRegistry.data[landMass].registry` null
|
||||
|
||||
**Hol kell javítani**: `Main.cs:755-774`
|
||||
|
||||
**Javítási javaslatok**:
|
||||
1. **NULL check hozzáadása** minden reflection művelet után:
|
||||
```csharp
|
||||
var landMassBuildingRegistry = __instance.GetType()...GetValue(__instance) as ArrayExt<...>;
|
||||
if (landMassBuildingRegistry == null) {
|
||||
Main.helper.Log("ERROR: landMassBuildingRegistry is null!");
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
2. **Array méret ellenőrzés**:
|
||||
```csharp
|
||||
if (landMass >= landMassBuildingRegistry.data.Length) {
|
||||
Main.helper.Log($"ERROR: landMass={landMass} >= array length={landMassBuildingRegistry.data.Length}");
|
||||
return false;
|
||||
}
|
||||
```
|
||||
|
||||
3. **Registry inicializálás ellenőrzés**:
|
||||
```csharp
|
||||
if (landMassBuildingRegistry.data[landMass] == null ||
|
||||
landMassBuildingRegistry.data[landMass].registry == null) {
|
||||
// Inicializálni vagy hibát logolni
|
||||
}
|
||||
```
|
||||
|
||||
**Kapcsolódó hiba**: Ez a hiba okozza a WorldPlace IndexOutOfRangeException-t is (lásd lent)
|
||||
|
||||
---
|
||||
|
||||
### 3. KRITIKUS: IndexOutOfRangeException WorldPlace-ben
|
||||
|
||||
**Fájl**: `Packets/Game/GameWorld/WorldPlace.cs`
|
||||
**Hiba helye**: `WorldPlace.cs:167-168`
|
||||
**Érintett kód**:
|
||||
```csharp
|
||||
// WorldPlace.cs:113
|
||||
player.inst.AddBuilding(building); // <-- Meghívja PlayerAddBuildingHook-ot (ami crash-el)
|
||||
|
||||
// WorldPlace.cs:167-168
|
||||
player.inst.LandMassNames[building.LandMass()] = player.kingdomName; // <-- CRASH ITT
|
||||
Player.inst.LandMassNames[building.LandMass()] = player.kingdomName;
|
||||
```
|
||||
|
||||
**Probléma részletesen**:
|
||||
- A README szerint: **9 IndexOutOfRangeException** - "Index was outside the bounds of the array"
|
||||
- **Ok-okozati lánc**:
|
||||
1. `WorldPlace.PlaceBuilding()` hívja `player.inst.AddBuilding(building)` (line 113)
|
||||
2. Ez triggereli a `PlayerAddBuildingHook.Prefix` metódust
|
||||
3. A hook crash-el NullReferenceException-nel (fenti #2 hiba)
|
||||
4. A try-catch elkapja (Main.cs:779-786), DE a building NEM kerül helyesen hozzáadásra
|
||||
5. A `WorldPlace.cs` folytatódik és megpróbálja indexelni: `LandMassNames[building.LandMass()]`
|
||||
6. **HA** a `LandMassNames` tömb nem inicializálva vagy túl kicsi → **IndexOutOfRangeException**
|
||||
|
||||
**Hol kell javítani**:
|
||||
1. **Elsődleges**: `Main.cs:764` - Javítani a PlayerAddBuildingHook-ot (lásd #2)
|
||||
2. **Másodlagos**: `WorldPlace.cs:167-168` - Védekező kód:
|
||||
```csharp
|
||||
int landMass = building.LandMass();
|
||||
|
||||
// Biztosítani hogy a LandMassNames tömb elég nagy
|
||||
while (player.inst.LandMassNames.Count <= landMass) {
|
||||
player.inst.LandMassNames.Add("");
|
||||
}
|
||||
while (Player.inst.LandMassNames.Count <= landMass) {
|
||||
Player.inst.LandMassNames.Add("");
|
||||
}
|
||||
|
||||
player.inst.LandMassNames[landMass] = player.kingdomName;
|
||||
Player.inst.LandMassNames[landMass] = player.kingdomName;
|
||||
```
|
||||
|
||||
**Miért kritikus**: Ez a hiba miatt **55/56 épület NEM kerül elhelyezésre** a kliensnél!
|
||||
|
||||
---
|
||||
|
||||
### Összefüggések
|
||||
|
||||
A három hiba összefügg:
|
||||
|
||||
```
|
||||
1. Host menübe lép
|
||||
→ Server NEM áll le (#1 hiba)
|
||||
→ Server tovább fogad packeteket
|
||||
|
||||
2. Kliens épületet helyez
|
||||
→ WorldPlace packet érkezik
|
||||
→ PlaceBuilding() meghívódik
|
||||
→ AddBuilding() triggereli PlayerAddBuildingHook-ot
|
||||
→ Hook crash-el NullReferenceException (#2 hiba)
|
||||
→ Building nem adódik hozzá helyesen
|
||||
|
||||
3. WorldPlace folytatódik
|
||||
→ LandMassNames[landMass] indexelés
|
||||
→ IndexOutOfRangeException (#3 hiba)
|
||||
→ Épület NEM jelenik meg
|
||||
```
|
||||
|
||||
**Eredmény**: 98% fail rate az épület elhelyezésben!
|
||||
|
||||
---
|
||||
|
||||
### Javítási prioritás
|
||||
|
||||
1. **#2 - PlayerAddBuildingHook** (LEGFONTOSABB) - Ez okozza a cascade failure-t
|
||||
2. **#3 - WorldPlace IndexOutOfRange** - Védekező kód hozzáadása
|
||||
3. **#1 - Server leállítás** - UX javítás, erőforrás kezelés
|
||||
|
||||
### Következő lépések
|
||||
|
||||
1. Ellenőrizni, hogy a `PlayerAddBuildingHook` null-ellenőrzései végig lefutnak, mielőtt az adatokat használjuk.
|
||||
2. Biztosítani, hogy a `Server.Stop()` meghívódik, amikor a játékos visszalép a menübe (`TransitionToHook`).
|
||||
3. Bővíteni a `WorldPlace` és a `LandMassNames` tömbök védelmét, hogy ne okozzon indexhiba az épület-telepítés során.
|
||||
### 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.
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using UnityEngine;
|
||||
|
||||
namespace KCM
|
||||
{
|
||||
internal static class ResourceStorageHelper
|
||||
{
|
||||
private static Type resourceStorageType;
|
||||
private static MethodInfo getComponentsMethod;
|
||||
private static MethodInfo isPrivateMethod;
|
||||
private static MethodInfo addResourceStorageMethod;
|
||||
|
||||
private static readonly Assembly[] candidateAssemblies = new[]
|
||||
{
|
||||
typeof(Player).Assembly,
|
||||
typeof(World).Assembly,
|
||||
typeof(FreeResourceManager).Assembly,
|
||||
};
|
||||
|
||||
private static void EnsureInitialized()
|
||||
{
|
||||
if (resourceStorageType != null)
|
||||
return;
|
||||
|
||||
foreach (var assembly in candidateAssemblies)
|
||||
{
|
||||
if (assembly == null)
|
||||
continue;
|
||||
|
||||
resourceStorageType = assembly.GetType("Assets.Interface.IResourceStorage", false);
|
||||
if (resourceStorageType != null)
|
||||
break;
|
||||
}
|
||||
|
||||
if (resourceStorageType == null)
|
||||
return;
|
||||
|
||||
getComponentsMethod = typeof(Component).GetMethod("GetComponents", new[] { typeof(Type) });
|
||||
isPrivateMethod = resourceStorageType.GetMethod("IsPrivate", BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
|
||||
addResourceStorageMethod = typeof(FreeResourceManager).GetMethod("AddResourceStorage", new[] { resourceStorageType });
|
||||
}
|
||||
|
||||
public static Component[] GetStorages(Component owner)
|
||||
{
|
||||
EnsureInitialized();
|
||||
if (resourceStorageType == null || getComponentsMethod == null)
|
||||
return Array.Empty<Component>();
|
||||
|
||||
return (Component[])getComponentsMethod.Invoke(owner, new object[] { resourceStorageType });
|
||||
}
|
||||
|
||||
public static bool IsPrivate(Component storage)
|
||||
{
|
||||
EnsureInitialized();
|
||||
if (isPrivateMethod == null)
|
||||
return true;
|
||||
|
||||
return (bool)isPrivateMethod.Invoke(storage, null);
|
||||
}
|
||||
|
||||
public static void Register(Component storage)
|
||||
{
|
||||
EnsureInitialized();
|
||||
if (addResourceStorageMethod == null || FreeResourceManager.inst == null)
|
||||
return;
|
||||
|
||||
addResourceStorageMethod.Invoke(FreeResourceManager.inst, new object[] { storage });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,18 +159,15 @@ namespace Riptide.Demos.Steam.PlayerHosted
|
||||
|
||||
Main.helper.Log("clear players");
|
||||
Main.kCPlayers.Clear();
|
||||
Main.clientSteamIds.Clear(); // Clear client-to-steam ID mapping
|
||||
Main.ClearVillagerPositionCache(); // Clear villager sync cache
|
||||
LobbyHandler.ClearPlayerList();
|
||||
LobbyHandler.ClearChatEntries();
|
||||
Main.helper.Log("end clear players");
|
||||
|
||||
// Reset loading state
|
||||
loadingSave = false;
|
||||
|
||||
if (KCServer.IsRunning)
|
||||
KCServer.server.Stop();
|
||||
|
||||
|
||||
|
||||
Main.TransitionTo(MenuState.ServerBrowser);
|
||||
ServerBrowser.registerServer = false;
|
||||
}
|
||||
|
||||
@@ -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?");
|
||||
|
||||
@@ -23,26 +23,34 @@ namespace KCM.ServerLobby
|
||||
{
|
||||
banner = transform.Find("PlayerBanner").GetComponent<RawImage>();
|
||||
|
||||
SetValues();
|
||||
|
||||
InvokeRepeating("SetValues", 0, 0.25f);
|
||||
|
||||
transform.Find("PlayerBanner").GetComponent<Button>().onClick.AddListener(() =>
|
||||
{
|
||||
Main.TransitionTo(MenuState.NameAndBanner);//ChooseBannerUI Hooks required, as well as townnameui
|
||||
});
|
||||
|
||||
SetValues();
|
||||
InvokeRepeating("SetValues", 0, 0.25f);
|
||||
}
|
||||
|
||||
public void SetValues()
|
||||
{
|
||||
try
|
||||
{
|
||||
KCPlayer player;
|
||||
Main.kCPlayers.TryGetValue(Main.GetPlayerByClientID(Client).steamId, out player);
|
||||
// First check if the client still exists
|
||||
if (!Main.TryGetPlayerByClientID(Client, out KCPlayer player) || player == null)
|
||||
{
|
||||
// Client no longer exists, stop the repeating invoke and destroy this entry
|
||||
CancelInvoke("SetValues");
|
||||
Destroy(gameObject);
|
||||
return;
|
||||
}
|
||||
|
||||
transform.Find("PlayerName").GetComponent<TextMeshProUGUI>().text = player.name;
|
||||
transform.Find("Ready").gameObject.SetActive(player.ready);
|
||||
|
||||
var bannerTexture = World.inst.liverySets[player.banner].banners;
|
||||
|
||||
|
||||
banner.texture = bannerTexture;
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
@@ -60,8 +60,6 @@ namespace KCM
|
||||
Falle
|
||||
}
|
||||
|
||||
bool awake = false;
|
||||
|
||||
public void Start()
|
||||
{
|
||||
Main.helper.Log("ServerLobby start called");
|
||||
@@ -144,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(() =>
|
||||
@@ -189,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)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using KCM.Packets;
|
||||
using KCM.Packets;
|
||||
using KCM.Packets.State;
|
||||
using KCM.StateManagement.Observers;
|
||||
using System;
|
||||
@@ -6,12 +6,15 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using UnityEngine;
|
||||
using static KCM.StateManagement.Observers.Observer;
|
||||
|
||||
namespace KCM.StateManagement.BuildingState
|
||||
{
|
||||
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,16 +26,31 @@ namespace KCM.StateManagement.BuildingState
|
||||
try
|
||||
{
|
||||
Observer observer = (Observer)sender;
|
||||
|
||||
Building building = (Building)observer.state;
|
||||
|
||||
Main.LogSync($"SENDING building state update for: {building.UniqueName} guid={building.guid}");
|
||||
Main.LogSync($" position={building.transform.position}");
|
||||
if (building.transform.childCount > 0)
|
||||
if (building == null)
|
||||
{
|
||||
Main.LogSync($" rotation={building.transform.GetChild(0).rotation} (euler={building.transform.GetChild(0).rotation.eulerAngles})");
|
||||
if(observer != null)
|
||||
{
|
||||
UnityEngine.Object.Destroy(observer.gameObject);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Guid guid = building.guid;
|
||||
|
||||
if (lastUpdateTime.ContainsKey(guid) && Time.time < lastUpdateTime[guid] + UpdateInterval)
|
||||
{
|
||||
return; // Not time to update yet
|
||||
}
|
||||
|
||||
if (!lastUpdateTime.ContainsKey(guid))
|
||||
lastUpdateTime.Add(guid, Time.time);
|
||||
else
|
||||
lastUpdateTime[guid] = Time.time;
|
||||
|
||||
//Main.helper.Log("Should send building network update for: " + building.UniqueName);
|
||||
|
||||
new BuildingStatePacket()
|
||||
{
|
||||
customName = building.customName,
|
||||
@@ -62,4 +80,4 @@ namespace KCM.StateManagement.BuildingState
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user