Compare commits
15 Commits
f03e13236f
...
b02af4d0c7
| Author | SHA1 | Date | |
|---|---|---|---|
| b02af4d0c7 | |||
| 5dba8137c3 | |||
| b3d7108574 | |||
| ce1c067fca | |||
| 5d086776cf | |||
| 89586ad8df | |||
| 8f3d83e807 | |||
| dd17030e56 | |||
| 8d599e13ad | |||
| eab7931f52 | |||
| 7d6c915b49 | |||
| f7fc5a3969 | |||
| 2f42cf9366 | |||
| 888c807b96 | |||
| bd12485112 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -23,4 +23,5 @@ Desktop.ini
|
||||
**/*.mdb
|
||||
**/*.pdb
|
||||
|
||||
/.claude
|
||||
/.claude
|
||||
/*.png
|
||||
104
CLAUDE.md
Normal file
104
CLAUDE.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
|
||||
This is a **Kingdoms and Castles Multiplayer Mod** that adds multiplayer functionality to the game using:
|
||||
- **Riptide Networking** library for low-level networking
|
||||
- **Steam P2P** transport for NAT traversal
|
||||
- **Harmony** for non-invasive game modification via patches/hooks
|
||||
|
||||
## Architecture
|
||||
|
||||
### Core Components
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `Main.cs` | Entry point, Harmony patches, all game event hooks |
|
||||
| `KCClient.cs` | Client-side networking wrapper around Riptide.Client |
|
||||
| `KCServer.cs` | Server-side networking, client management |
|
||||
| `KCPlayer.cs` | Player data container (id, steamId, inst, kingdomName) |
|
||||
|
||||
### Networking Layer
|
||||
|
||||
```
|
||||
Riptide.Client/Server
|
||||
└── SteamClient/SteamServer (Steam P2P transport)
|
||||
└── KCClient/KCServer wrappers
|
||||
└── PacketHandler (serialization/routing)
|
||||
```
|
||||
|
||||
- Port: 7777, Max clients: 25
|
||||
- Team ID formula: `clientId * 10 + 2`
|
||||
|
||||
### Packet System
|
||||
|
||||
Located in `/Packets/`:
|
||||
- Base class: `Packet.cs` with `Send()`, `SendToAll()`, `HandlePacketClient()`, `HandlePacketServer()`
|
||||
- `PacketHandler.cs` uses reflection for automatic serialization based on property names (alphabetical order)
|
||||
- Packet IDs defined in `Enums/Packets.cs`
|
||||
|
||||
Key packet ranges:
|
||||
- 25-34: Lobby (chat, player list, settings)
|
||||
- 70-79: World/building updates
|
||||
- 85: Save transfer (chunked)
|
||||
- 87-90: Building state, villagers
|
||||
|
||||
### State Synchronization
|
||||
|
||||
- **Buildings**: Observer pattern in `StateManagement/BuildingState/` - monitors field changes every 100ms, sends updates every 300ms
|
||||
- **Villagers**: Event-based sync via Harmony hooks on `VillagerSystem.AddVillager`, `Villager.TeleportTo`
|
||||
- **Save/Load**: Custom `MultiplayerSaveContainer` extends `LoadSaveContainer`, stores per-player data
|
||||
|
||||
### Harmony Hooks Pattern
|
||||
|
||||
All hooks check call stack to prevent infinite loops:
|
||||
```csharp
|
||||
if (new StackFrame(3).GetMethod().Name.Contains("HandlePacket"))
|
||||
return; // Skip if called by network handler
|
||||
```
|
||||
|
||||
### Key Dictionaries
|
||||
|
||||
```csharp
|
||||
Main.kCPlayers // Dictionary<steamId, KCPlayer>
|
||||
Main.clientSteamIds // Dictionary<clientId, steamId>
|
||||
```
|
||||
|
||||
## Common Issues & Patterns
|
||||
|
||||
### Player Resolution
|
||||
```csharp
|
||||
Main.GetPlayerByClientID(clientId) // clientId -> KCPlayer
|
||||
Main.GetPlayerByTeamID(teamId) // teamId -> Player.inst
|
||||
Main.GetPlayerByBuilding(building) // building -> owner Player
|
||||
```
|
||||
|
||||
### Building Ownership
|
||||
Buildings are associated with players via `LandmassOwner.teamId`. Use `building.TeamID()` to determine owner.
|
||||
|
||||
### Save Directory
|
||||
Multiplayer saves go to: `Application.persistentDataPath + "/Saves/Multiplayer"`
|
||||
|
||||
## Directory Structure
|
||||
|
||||
```
|
||||
/Attributes - Custom packet attributes
|
||||
/Enums - Packet types, menu states
|
||||
/LoadSaveOverrides - MultiplayerSaveContainer
|
||||
/Packets - All network packets
|
||||
/Riptide - Networking library
|
||||
/RiptideSteamTransport - Steam P2P adapter, LobbyManager
|
||||
/StateManagement - Observer pattern for sync
|
||||
/ServerLobby - Lobby UI
|
||||
/ServerBrowser - Server discovery
|
||||
/UI - Custom UI elements
|
||||
```
|
||||
|
||||
## Known Architecture Limitations
|
||||
|
||||
1. Static `Client`/`Server` instances can cause issues on reconnect
|
||||
2. Call stack checking for loop prevention is fragile
|
||||
3. No conflict resolution - last-write-wins
|
||||
4. Villager sync is event-based only, no continuous state updates
|
||||
28
KCServer.cs
28
KCServer.cs
@@ -18,18 +18,22 @@ namespace KCM
|
||||
{
|
||||
public class KCServer : MonoBehaviour
|
||||
{
|
||||
public static Server server = new Server(Main.steamServer);
|
||||
public static Server server = null;
|
||||
public static bool started = false;
|
||||
|
||||
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;
|
||||
|
||||
@@ -95,16 +99,18 @@ namespace KCM
|
||||
}
|
||||
}*/
|
||||
|
||||
public static bool IsRunning { get { return server.IsRunning; } }
|
||||
public static bool IsRunning { get { return server != null && server.IsRunning; } }
|
||||
|
||||
private void Update()
|
||||
{
|
||||
server.Update();
|
||||
if (server != null)
|
||||
server.Update();
|
||||
}
|
||||
|
||||
private void OnApplicationQuit()
|
||||
{
|
||||
server.Stop();
|
||||
if (server != null && server.IsRunning)
|
||||
server.Stop();
|
||||
}
|
||||
|
||||
private void Preload(KCModHelper helper)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Assets.Code;
|
||||
using Assets.Interface;
|
||||
using Riptide;
|
||||
using Riptide.Transports;
|
||||
using Steamworks;
|
||||
@@ -255,6 +256,47 @@ 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 = building.GetComponents<IResourceStorage>();
|
||||
foreach (var storage in storages)
|
||||
{
|
||||
if (storage != null && !storage.IsPrivate())
|
||||
{
|
||||
try
|
||||
{
|
||||
FreeResourceManager.inst.AddResourceStorage(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;
|
||||
}
|
||||
}
|
||||
|
||||
102
Main.cs
102
Main.cs
@@ -183,39 +183,13 @@ namespace KCM
|
||||
|
||||
public static int FixedUpdateInterval = 0;
|
||||
|
||||
public static void ClearVillagerPositionCache()
|
||||
{
|
||||
// Kept for API compatibility with LobbyManager
|
||||
}
|
||||
|
||||
private void FixedUpdate()
|
||||
{
|
||||
// send batched building placement info
|
||||
/*if (PlaceHook.QueuedBuildings.Count > 0 && (FixedUpdateInterval % 25 == 0))
|
||||
{
|
||||
foreach (Building building in PlaceHook.QueuedBuildings)
|
||||
{
|
||||
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();
|
||||
}
|
||||
|
||||
PlaceHook.QueuedBuildings.Clear();
|
||||
}*/
|
||||
|
||||
FixedUpdateInterval++;
|
||||
}
|
||||
|
||||
@@ -727,6 +701,8 @@ namespace KCM
|
||||
}
|
||||
LogStep();
|
||||
|
||||
// CRITICAL: Bake pathing for villager movement!
|
||||
b.BakePathing();
|
||||
|
||||
return false;
|
||||
}
|
||||
@@ -780,6 +756,7 @@ namespace KCM
|
||||
new AddVillagerPacket()
|
||||
{
|
||||
guid = __result.guid,
|
||||
position = pos, // Include villager spawn position
|
||||
}.Send();
|
||||
}
|
||||
catch (Exception e)
|
||||
@@ -1417,34 +1394,55 @@ namespace KCM
|
||||
{
|
||||
if (KCClient.client.IsConnected)
|
||||
{
|
||||
Main.helper.Log($"[ProcessBuilding] START - {structureData.uniqueName}");
|
||||
|
||||
Building Building = GameState.inst.GetPlaceableByUniqueName(structureData.uniqueName);
|
||||
bool flag = Building;
|
||||
if (flag)
|
||||
try
|
||||
{
|
||||
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);
|
||||
Building Building = GameState.inst.GetPlaceableByUniqueName(structureData.uniqueName);
|
||||
Main.helper.Log($"[ProcessBuilding] GetPlaceable: {(Building != null ? "OK" : "NULL")}");
|
||||
|
||||
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)
|
||||
if (Building)
|
||||
{
|
||||
p.keep = building.GetComponent<Keep>();
|
||||
Main.helper.Log(p.keep.ToString());
|
||||
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;
|
||||
}
|
||||
__result = building;
|
||||
}
|
||||
else
|
||||
catch (Exception e)
|
||||
{
|
||||
Main.helper.Log(structureData.uniqueName + " failed to load correctly");
|
||||
Main.helper.Log($"[ProcessBuilding] EXCEPTION: {structureData.uniqueName}");
|
||||
Main.helper.Log($"[ProcessBuilding] Error: {e.Message}");
|
||||
Main.helper.Log($"[ProcessBuilding] Stack: {e.StackTrace}");
|
||||
__result = null;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ namespace KCM.Packets.Game.GamePlayer
|
||||
public override ushort packetId => (ushort)Enums.Packets.AddVillager;
|
||||
|
||||
public Guid guid { get; set; }
|
||||
public Vector3 position { get; set; }
|
||||
|
||||
public override void HandlePacketClient()
|
||||
{
|
||||
@@ -19,18 +20,33 @@ namespace KCM.Packets.Game.GamePlayer
|
||||
{
|
||||
if (KCClient.client.Id == clientId) return;
|
||||
|
||||
// Check for duplicate villager by guid
|
||||
var existingVillager = player.inst.Workers.data.FirstOrDefault(w => w != null && w.guid == guid);
|
||||
if (existingVillager != null)
|
||||
{
|
||||
Main.helper.Log($"Villager with guid {guid} already exists, skipping duplicate");
|
||||
return;
|
||||
}
|
||||
|
||||
Main.helper.Log("Received add villager packet from " + player.name + $"({player.id})");
|
||||
|
||||
Villager v = Villager.CreateVillager();
|
||||
v.guid = guid;
|
||||
Villager newVillager = Villager.CreateVillager();
|
||||
newVillager.guid = guid;
|
||||
|
||||
player.inst.Workers.Add(v);
|
||||
player.inst.Homeless.Add(v);
|
||||
// Set villager position
|
||||
if (position != Vector3.zero)
|
||||
{
|
||||
newVillager.TeleportTo(position);
|
||||
}
|
||||
|
||||
player.inst.Workers.Add(newVillager);
|
||||
player.inst.Homeless.Add(newVillager);
|
||||
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Main.helper.Log("Error handling add villager packet: " + e.Message);
|
||||
Main.helper.Log(e.StackTrace);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,14 @@ namespace KCM.Packets.Game.GameWorld
|
||||
{
|
||||
Main.helper.Log("Received place building packet for " + uniqueName + " from " + player.name + $"({player.id})");
|
||||
|
||||
// Check for duplicate building by guid to prevent double placement from network retries
|
||||
var existingBuilding = player.inst.Buildings.data.FirstOrDefault(b => b != null && b.guid == guid);
|
||||
if (existingBuilding != null)
|
||||
{
|
||||
Main.helper.Log($"Building with guid {guid} already exists for player {player.name}, skipping duplicate placement");
|
||||
return;
|
||||
}
|
||||
|
||||
//var originalPlayer = Player.inst;
|
||||
//Player.inst = player.inst;
|
||||
|
||||
|
||||
75
README.md
75
README.md
@@ -1,75 +0,0 @@
|
||||
# KCM (Kingdoms and Castles Multiplayer)
|
||||
|
||||
Ez a repó egy *Kingdoms and Castles* multiplayer mod forrása. A mod Steam lobby + Riptide alapú hálózattal próbálja a világot és a játékosok akcióit több kliens között szinkronban tartani.
|
||||
|
||||
Ha a `output.txt` logban `Compilation failed` szerepel, akkor a mod **nem töltődött be**, és semmi nem fog szinkronizálódni (ilyenkor tipikusan C# szintaxis / runtime-kompatibilitási hiba van a forrásban).
|
||||
|
||||
## Mit szinkronizál a mod? (jelenlegi állapot)
|
||||
|
||||
**Lobby / kapcsolat**
|
||||
- Játékos csatlakozás/leválás, player lista, ready állapot.
|
||||
- Szerver beállítások (név, max players, seed, world opciók).
|
||||
- Chat és rendszerüzenetek.
|
||||
|
||||
**Világ indítás**
|
||||
- World seed szétküldése és world generálás a klienseken.
|
||||
- (Beállítástól függően) keep elhelyezés csomagból.
|
||||
|
||||
**Gameplay alap**
|
||||
- Épület lerakás események (alap meta: `uniqueName`, `guid`, pozíció/rotáció).
|
||||
- Épület állapot frissítések “snapshot” jelleggel (`BuildingStatePacket`): built/placed, constructionProgress, life, stb.
|
||||
- Néhány globális esemény: idősebesség változtatás, időjárás váltás, fa kivágás (repo verziótól függően).
|
||||
- Host oldalon periodikus *resource snapshot* korrigálás (ha drift/desync van, visszahúzza a klienst).
|
||||
|
||||
**Mentés betöltés (host → kliens)**
|
||||
- Host oldalon a mentés byte-ok chunkolva kerülnek kiküldésre (`SaveTransferPacket`).
|
||||
- Kliens oldalon érkezés után `LoadSave.Load()` + `MultiplayerSaveContainer.Unpack()` fut.
|
||||
- Ha a kiválasztott mentés nem multiplayer container (vanilla mentés), a host fallback-ként átadja a normál betöltést.
|
||||
|
||||
## Mi nincs (még) rendesen szinkronizálva? (gyakori desync okok)
|
||||
|
||||
Ezek okozzák a tipikus “farm termel, de nem látszik” / “resource nem frissül” / “animáció hiányzik” jelenségeket:
|
||||
- **Erőforrás-logika és szállítás**: raktárkészletek, haul/cart routing, villager “viszem/lerakom” animációk nincsenek teljes állapotban szinkronizálva.
|
||||
- **Villager/job részletek**: current task, target, carried resource, pathing cache, részfeladat-állapot.
|
||||
- **Field/Farm belső állapot**: growth stage, harvest queue, field regisztráció edge case-ek.
|
||||
- **UI / kliens oldali state**: beragadt menük, promptok (pl. “rakd le a kezdő épületet”), lokális UI state nem hálózati adat.
|
||||
- **AI brains / nem-player rendszerek**: részben vagy egyáltalán nincs “szerver az igazság” modell.
|
||||
|
||||
## Mit érdemes még hozzáadni? (roadmap)
|
||||
|
||||
Ha cél a stabil “load utáni sync” és kevesebb vizuális desync:
|
||||
- **Resource szinkron**: raktárak készlete, termelés/fogyasztás tick eredménye, szállítási queue események (event-based vagy periodikus snapshot).
|
||||
- **Villager szinkron**: villager state machine + carried resource + célpont; vagy determinisztikus szerver oldali szimuláció és kliens “replay”.
|
||||
- **Farm/Field szinkron**: field állapot (growth/ready/harvest), aratás események explicit hálózati üzenetként.
|
||||
- **Robusztus reconnect**: kilépés egy sessionből → másik lobby csatlakozás restart nélkül (minden statikus állapot, observer, transfer state, player cache teljes resetje).
|
||||
- **Debug eszközök**: desync detektor (hash/snapshot összehasonlítás), több log a load/sync pontokra.
|
||||
|
||||
## Telepítés
|
||||
|
||||
- Hostnak és **minden kliensnek ugyanaz a mod verzió** kell.
|
||||
- Workshop verzió frissítés felülírhatja a módosításokat. Ajánlott:
|
||||
- kimásolni a modot a játék `...\\KingdomsAndCastles_Data\\mods\\` mappájába egy külön névvel,
|
||||
- és a mod menüben kikapcsolni a Workshop verziót.
|
||||
- Változtatások után **teljes játék újraindítás** javasolt.
|
||||
|
||||
## Hibaelhárítás
|
||||
|
||||
**Log helye:** a mod mappájában gyakran van `output.txt`.
|
||||
|
||||
Nézd ezeket a kulcssorokat:
|
||||
- `Compilation failed` → a mod nem fordult le, nincs multiplayer.
|
||||
- `Save Transfer started/complete` → mentés átküldés/betöltés állapota.
|
||||
- `Error loading save` / `LoadError` → sérült/rossz típusú mentés, vagy verzió eltérés.
|
||||
|
||||
Bug reporthoz küldd el:
|
||||
- a hiba környéki 50–100 sort a `output.txt`-ből,
|
||||
- host/kliens szerep, játék verzió, mod verzió,
|
||||
- új világban vagy mentés betöltés után jelentkezik-e.
|
||||
|
||||
## Fejlesztés
|
||||
|
||||
Repo-szabályok és szerkezet: `AGENTS.md`.
|
||||
|
||||
### Gyors resync
|
||||
|
||||
A lobby chatben írd be: `/resync` – a kliens kér egy resync-et a hosttól (resource + building + villager “teleport” snapshot).
|
||||
|
||||
@@ -159,15 +159,18 @@ 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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user