Compare commits
23 Commits
62db70c1c4
...
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 |
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
|
||||
}
|
||||
}
|
||||
|
||||
132
Main.cs
132
Main.cs
@@ -57,7 +57,21 @@ namespace KCM
|
||||
|
||||
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
|
||||
@@ -166,11 +180,6 @@ namespace KCM
|
||||
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
|
||||
{
|
||||
|
||||
@@ -180,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)
|
||||
{
|
||||
@@ -264,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);
|
||||
}
|
||||
@@ -298,9 +356,6 @@ namespace KCM
|
||||
helper.Log("Preload start in main");
|
||||
try
|
||||
{
|
||||
|
||||
|
||||
//MainMenuPatches.Patch();
|
||||
Main.helper = helper;
|
||||
helper.Log(helper.modPath);
|
||||
|
||||
@@ -435,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++)
|
||||
@@ -1295,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;
|
||||
}
|
||||
|
||||
@@ -1661,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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}*/
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ namespace KCM.Packets
|
||||
{
|
||||
if (KCServer.IsRunning && toClient != 0)
|
||||
{
|
||||
KCServer.server.Send(PacketHandler.SerialisePacket(this), toClient, Riptide.MessageSendMode.Reliable);
|
||||
KCServer.server.Send(PacketHandler.SerialisePacket(this), toClient, true);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
|
||||
38
README.md
Normal file
38
README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Kingdoms and Castles Multiplayer Mod Fixes
|
||||
|
||||
This document summarizes the fixes and improvements implemented to enhance the stability and functionality of the multiplayer mod for Kingdoms and Castles.
|
||||
|
||||
## Implemented Fixes:
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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).
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.
|
||||
|
||||
## Pending Task:
|
||||
|
||||
### 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.
|
||||
@@ -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?");
|
||||
|
||||
@@ -37,8 +37,15 @@ namespace KCM.ServerLobby
|
||||
{
|
||||
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);
|
||||
|
||||
|
||||
@@ -60,8 +60,6 @@ namespace KCM
|
||||
Falle
|
||||
}
|
||||
|
||||
bool awake = false;
|
||||
|
||||
public void Start()
|
||||
{
|
||||
Main.helper.Log("ServerLobby start called");
|
||||
@@ -188,14 +186,30 @@ namespace KCM
|
||||
StartGameButton.GetComponentInChildren<TextMeshProUGUI>().text = "Start";
|
||||
StartGameButton.onClick.AddListener(() =>
|
||||
{
|
||||
if (World.inst.GetTextSeed() != WorldSeed.text)
|
||||
int definitiveSeed;
|
||||
if (string.IsNullOrWhiteSpace(WorldSeed.text))
|
||||
{
|
||||
World.inst.seed = World.inst.SeedFromText(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 = World.inst.seed
|
||||
Seed = definitiveSeed
|
||||
}.SendToAll(KCClient.client.Id);
|
||||
|
||||
new StartGame().SendToAll();
|
||||
|
||||
@@ -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