增加热作逻辑

This commit is contained in:
wuwenbo 2026-06-08 16:55:24 +08:00
parent 1363d7c2e0
commit d79f1f9519
7 changed files with 230 additions and 29 deletions

View File

@ -103,6 +103,7 @@ AI 的最终执行仍然走 action 层:
多人行为不要绕过 `CompleteExecute`
- 判断本机是否能执行当前回合 action 时,优先使用 `MapData.CanLocalControlPlayer(playerId)`,不要只比较 `CurPlayer == PlayerMap.SelfPlayerData`。单机/房主可以控制多个真实 player 时,`SelfPlayerId` 会为旧 UI/input 临时切到当前本机可控玩家;非本机可控回合应恢复默认本机 player避免资源、投降、存活等待等 UI 看错对象。
- Host 本地 action`CompleteExecute``ActionExecute` 广播,广播失败则不执行本地 mutation。
- Client 本地 action`CompleteExecute` 发送 `ActionConfirm` 给 host发送失败不执行。本地 `TurnEnd` 发送后直接返回 false等待 host 广播。
- Host 收到 `ActionConfirm`:刷新 params校验当前玩家调用 `CompleteExecute`,再广播 `ActionExcuteMessage` 并执行。

View File

@ -44,6 +44,12 @@ Archive files are grouped by folder:
`Quick` uses fixed record id and file id `quick`. Manual/end/begin ids are GUID-like strings from `GameArchiveManager`.
Record persistence:
- `Manual` and `Quick` records are persistent by default and should stay persistent after old data migration.
- `Ended` records are non-persistent by default; only explicit user/UI action should persist them.
- Append new `GameRecord` fields at the end only. `IsPersistent` belongs after archive id fields.
## Write Flow
- New single-player game: write begin after map generation, before first turn refresh.
@ -51,12 +57,27 @@ Archive files are grouped by folder:
- Ended game: `FinishState.Enter` calls `GameArchiveManager.SaveEndRecord(Main.MapData)`.
- Per-turn quick save: `MapData.RefreshTurn` calls `SaveQuickContinueRecord`.
- Manual save: UI or upper layer should call `GameArchiveManager.SaveManualGameRecord(recordName)`.
- Manual delete: call `GameArchiveManager.DeleteManualGameRecord(record)`; do not delete quick/end records through this path.
- Record delete: call `GameArchiveManager.DeleteGameRecord(record)` for any non-`Quick` record. This must only remove the record index, not archive files.
Do not save archives directly from surrender action. Surrender should mutate game state through the action flow; `AfterExecute` refreshes settlement, `GameLogic.Update` enters `Finished`, and `FinishState.Enter` writes end and clears quick.
`SaveEndRecord` writes end, creates an `Ended` record except for Tutor, and deletes the current begin's quick record/file. Manual saves survive game end until the player deletes them.
## Record Cleanup
Keep record operations and archive-file operations separate:
- `GameArchiveManager.SetGameRecordPersistent(record, bool)` may only change `Ended` records.
- `GameArchiveManager.DeleteGameRecord(record)` may delete `Manual` or `Ended` records, never `Quick`, and must not delete `MapData` files.
- `GameArchiveManager.CleanupNonPersistentGameRecords()` removes non-persistent records only. With defaults, this mostly cleans unpinned `Ended` records.
- `GameArchiveManager.CleanupUnlinkedLocalMapData()` removes local `MapData` files not referenced by any remaining record.
Archive cleanup should cover both current and legacy local data:
- current `Config/GameArchives/begin`, `quick_continue`, `continue`, and `end`;
- legacy `Config/map_archive_begin|continue|end[_multi]_{MapID}.dat` and sidecars;
- old `Config/begin`, `continue`, `end`, `begincontinue` / `begin_continue`, and `quick_continue` folders when they contain archive payloads.
## Resume Flow
Public resume should go through:

View File

@ -62,14 +62,18 @@ For the one-click Steam P2P stress tool, report fields, and current healthy base
6. Keep lobby `MapConfig` host-authoritative.
- Pre-game room settings, `MemberCiv`, player slot/team/AI flags, and ready state live in `Main.Instance.MapConfig`.
- `MapConfig.MultiCivs` is now the full player-slot list and should be sized to `PlayerCount`; do not treat it as only current lobby members.
- Each `MemberCiv` slot uses `Index` as the stable player position. `MemberId != 0` means a real member is bound, `MemberId == 0 && IsAI` means an AI slot, and `TeamId == 0` means no team.
- Each `MemberCiv` slot uses `Index` as the stable player position. `MemberId != 0` means a real member is bound, `MemberId == 0 && IsAI` means an AI slot, `MemberId == 0 && !IsAI && IsHostControlled` means a host/local-controlled real player slot, and `TeamId == 0` means no team.
- Host-controlled slots are real players, not AI and not lobby members. Keep `IsReady=false`, do not add them to `Net.Players`, skip them when assigning/moving real lobby members, and allow them in `AreAllLobbyMembersReady()`.
- `Net.RefreshPlayerNet(mapData)` maps only current lobby `MemberId` values to `PlayerId`; host-controlled slots must not require a Steam member mapping or duplicate the host's mapping.
- Local control should go through `MapData.CanLocalControlPlayer(...)`: in singleplayer all real local slots are controllable; in multiplayer the member's mapped slot is controllable, and only the lobby owner may also control `IsHostControlled` slots. When switching turn identity for UI/input, restore the default local member slot on non-local turns.
- Call `MapConfig.EnsurePlayerSlots(NetMode.Multi)` before reading or mutating lobby slots, especially after changing `PlayerCount` or receiving host config.
- Clients may optimistically update their own `MemberCiv` only after `ChangeCiv` send succeeds, then still accept host `UpdateLobbyData` as authority.
- Clients entering a room should request host lobby data until current lobby members match `MapConfig.MultiCivs`.
- Host must refresh lobby members before sending lobby config; new guests default not ready, and the owner is always ready.
- Changing civ, team, AI slot ownership, slot assignment, or host room settings should clear guest ready state.
- UI room-row reconciliation must count host-controlled slots as occupied seats, not open seats, and must not auto-convert them between open and AI.
- Host start/resume must require `AreAllLobbyMembersReady()`.
- `Net.RefreshPlayerNet(mapData)` maps real lobby `MemberId` values to the slot-created `PlayerId`; AI slots must not require a lobby member mapping.
- `Net.RefreshPlayerNet(mapData)` maps real lobby `MemberId` values to the slot-created `PlayerId`; AI and host-controlled slots must not require a lobby member mapping.
- `TeamId` drives in-game teammate diplomacy, so host and clients must agree on the full slot list before `GameStart`.
7. Roll back failed start/resume.
@ -115,7 +119,8 @@ For network-heavy changes, inspect these risks explicitly:
- Could a caller ignore a failed send and still mutate game state?
- Could a client treat optimistic `MemberCiv` or ready state as authoritative before host `UpdateLobbyData`?
- Could the host start/resume while any guest is not ready or current lobby members do not match `MapConfig.MultiCivs`?
- Could an empty non-AI slot (`MemberId == 0 && !IsAI`) start the game accidentally?
- Could an empty non-AI slot (`MemberId == 0 && !IsAI && !IsHostControlled`) start the game accidentally?
- Could a host-controlled slot be counted as an open UI seat, converted to AI/open by reconciliation, or inserted into `Net.Players`?
- Could code reintroduce the old assumption that `MultiCivs.Count == current lobby member count`?
- Could `MemberCiv.Index`, `PlayerId`, or `TeamId` diverge between host and clients before `GameStart`?
- Could `MapData` deserialize with missing core fields and still be used?

View File

@ -224,7 +224,7 @@ namespace RuntimeData
private static bool CanRemovePlayerSlot(MemberCiv slot)
{
return slot == null || slot.MemberId == 0 && slot.IsAI;
return slot == null || slot.MemberId == 0 && slot.IsAI && !slot.IsHostControlled;
}
private MemberCiv CreateDefaultPlayerSlot(int index, NetMode netMode)
@ -239,7 +239,8 @@ namespace RuntimeData
TeamId = NoTeamId,
IsAI = true,
IsReady = false,
IsCivFixed = false
IsCivFixed = false,
IsHostControlled = false
};
return slot;
@ -251,6 +252,7 @@ namespace RuntimeData
var selfSlot = MultiCivs[0];
PrepareSinglePlayerSlot(selfSlot, index: 0, isAI: false);
selfSlot.IsHostControlled = true;
selfSlot.IsCivFixed = true;
var usedCivs = new HashSet<uint> { selfSlot.CivId };
@ -277,6 +279,7 @@ namespace RuntimeData
slot.PlayerId = 0;
slot.TeamId = NoTeamId;
slot.IsAI = isAI;
slot.IsHostControlled = !isAI;
}
private static uint PickDefaultCivId(int preferredIndex, HashSet<uint> usedCivs)
@ -295,7 +298,12 @@ namespace RuntimeData
{
slot.Index = index;
if (slot.TeamId < NoTeamId) slot.TeamId = NoTeamId;
if (slot.MemberId != 0) slot.IsAI = false;
if (slot.MemberId != 0)
{
slot.IsAI = false;
slot.IsHostControlled = false;
}
if (slot.IsAI) slot.IsHostControlled = false;
if (slot.MemberId == 0) slot.PlayerId = 0;
}
@ -303,7 +311,12 @@ namespace RuntimeData
{
slot.Index = index;
if (slot.TeamId < NoTeamId) slot.TeamId = NoTeamId;
if (slot.MemberId != 0) slot.IsAI = false;
if (slot.MemberId != 0)
{
slot.IsAI = false;
slot.IsHostControlled = false;
}
if (slot.IsAI) slot.IsHostControlled = false;
}
// 根据房间成员信息更新 mapconfig 信息
@ -336,6 +349,7 @@ namespace RuntimeData
slot.MemberId = kv.Key;
slot.PlayerId = 0;
slot.IsAI = false;
slot.IsHostControlled = false;
slot.IsReady = LobbyManager.Instance.Lobby.IsInLobby()
&& kv.Key == LobbyManager.Instance.Lobby.GetLobbyOwnerId();
changed = true;
@ -350,6 +364,7 @@ namespace RuntimeData
foreach (var slot in MultiCivs)
{
if (slot == null || slot.MemberId != 0) continue;
if (slot.IsHostControlled) continue;
return slot;
}
@ -362,6 +377,7 @@ namespace RuntimeData
slot.PlayerId = 0;
slot.IsReady = false;
slot.IsAI = makeAi;
slot.IsHostControlled = false;
if (!clearCiv) return;
slot.CivId = 0;
slot.ForceId = 0;
@ -445,6 +461,31 @@ namespace RuntimeData
return true;
}
public bool SetPlayerSlotHostControlled(int index, bool isHostControlled, NetMode netMode = NetMode.Multi)
{
if (netMode == NetMode.Multi
&& LobbyManager.Instance.Lobby.IsInLobby()
&& !LobbyManager.Instance.Lobby.IsLobbyOwner())
return false;
var slot = GetPlayerSlot(index, netMode);
if (slot == null) return false;
if (isHostControlled)
{
if (slot.MemberId != 0) return false;
if (slot.IsHostControlled && !slot.IsAI) return false;
slot.MemberId = 0;
slot.PlayerId = 0;
slot.IsAI = false;
slot.IsHostControlled = true;
slot.IsReady = false;
return true;
}
if (!slot.IsHostControlled) return false;
slot.IsHostControlled = false;
return true;
}
public bool SetPlayerSlotCiv(int index, uint civId, uint forceId, NetMode netMode = NetMode.Multi)
{
var slot = GetPlayerSlot(index, netMode);
@ -493,12 +534,14 @@ namespace RuntimeData
var current = GetMemberCiv(memberId);
var target = MultiCivs[index];
if (target.MemberId != 0 && target.MemberId != memberId) return false;
if (target.IsHostControlled) return false;
if (current == target) return false;
if (current != null) ClearPlayerSlotMember(current, makeAi: true, clearCiv: true);
target.MemberId = memberId;
target.PlayerId = 0;
target.IsAI = false;
target.IsHostControlled = false;
target.IsReady = false;
EnsureLobbyOwnerReady();
RefreshMultiCivsDict();
@ -519,6 +562,30 @@ namespace RuntimeData
return false;
}
public bool IsHostControlledPlayer(uint playerId)
{
if (playerId == 0 || MultiCivs == null) return false;
foreach (var slot in MultiCivs)
{
if (slot == null || slot.PlayerId != playerId) continue;
return slot.IsHostControlled;
}
return false;
}
public bool IsRealPlayerSlot(uint playerId)
{
if (playerId == 0 || MultiCivs == null) return false;
foreach (var slot in MultiCivs)
{
if (slot == null || slot.PlayerId != playerId) continue;
return slot.MemberId != 0 || slot.IsHostControlled;
}
return false;
}
public bool ArePlayersInSameTeam(uint playerIdA, uint playerIdB)
{
if (playerIdA == 0 || playerIdB == 0 || playerIdA == playerIdB) return false;
@ -615,6 +682,7 @@ namespace RuntimeData
var targetSlot = GetPlayerSlot(index, NetMode.Multi);
if (targetSlot == null) return false;
if (targetSlot.MemberId != 0 && targetSlot.MemberId != selfMemberId) return false;
if (targetSlot.IsHostControlled) return false;
var next = CopyMemberCiv(memberCiv);
next.Index = index;
@ -657,6 +725,7 @@ namespace RuntimeData
var targetSlot = GetPlayerSlot(index, NetMode.Multi);
if (targetSlot == null) return false;
if (targetSlot.MemberId != 0 && targetSlot.MemberId != selfMemberId) return false;
if (targetSlot.IsHostControlled) return false;
var next = CopyMemberCiv(memberCiv);
next.Index = index;
@ -680,7 +749,8 @@ namespace RuntimeData
TeamId = memberCiv.TeamId,
IsAI = memberCiv.IsAI,
IsCivFixed = memberCiv.IsCivFixed,
IsReady = memberCiv.IsReady
IsReady = memberCiv.IsReady,
IsHostControlled = memberCiv.IsHostControlled
};
}
@ -701,7 +771,8 @@ namespace RuntimeData
TeamId = memberCiv.TeamId,
IsAI = memberCiv.IsAI,
IsCivFixed = memberCiv.IsCivFixed,
IsReady = memberId == ownerId || isReady
IsReady = memberId == ownerId || isReady,
IsHostControlled = memberCiv.IsHostControlled
};
return UpdateMemberCiv(next);
}
@ -738,7 +809,7 @@ namespace RuntimeData
foreach (var slot in MultiCivs)
{
if (slot == null) return false;
if (slot.MemberId == 0 && !slot.IsAI) return false;
if (slot.MemberId == 0 && !slot.IsAI && !slot.IsHostControlled) return false;
}
var ownerId = LobbyManager.Instance.Lobby.GetLobbyOwnerId();
@ -783,7 +854,7 @@ namespace RuntimeData
var targetIndex = civ.Index;
var targetSlot = targetIndex >= 0 && targetIndex < MultiCivs.Count ? MultiCivs[targetIndex] : null;
if (memberCiv != null && targetSlot != null && memberCiv != targetSlot && targetSlot.MemberId == 0)
if (memberCiv != null && targetSlot != null && memberCiv != targetSlot && targetSlot.MemberId == 0 && !targetSlot.IsHostControlled)
{
ClearPlayerSlotMember(memberCiv, makeAi: true, clearCiv: true);
memberCiv = targetSlot;
@ -795,6 +866,7 @@ namespace RuntimeData
var nextMemberId = civ.MemberId;
var nextIsAI = nextMemberId == 0 && civ.IsAI;
if (nextMemberId != 0) nextIsAI = false;
var nextIsHostControlled = nextMemberId == 0 && !nextIsAI && civ.IsHostControlled;
if (memberCiv.MemberId == nextMemberId
&& memberCiv.CivId == civ.CivId
@ -803,7 +875,8 @@ namespace RuntimeData
&& memberCiv.TeamId == civ.TeamId
&& memberCiv.IsAI == nextIsAI
&& memberCiv.IsCivFixed == civ.IsCivFixed
&& memberCiv.IsReady == civ.IsReady) return false;
&& memberCiv.IsReady == civ.IsReady
&& memberCiv.IsHostControlled == nextIsHostControlled) return false;
memberCiv.MemberId = nextMemberId;
memberCiv.CivId = civ.CivId;
@ -813,6 +886,7 @@ namespace RuntimeData
memberCiv.IsAI = nextIsAI;
memberCiv.IsCivFixed = civ.IsCivFixed;
memberCiv.IsReady = civ.IsReady;
memberCiv.IsHostControlled = nextIsHostControlled;
NormalizePlayerSlot(memberCiv, memberCiv.Index);
RefreshMultiCivsDict();
return true;
@ -915,6 +989,7 @@ namespace RuntimeData
public int TeamId;
public bool IsAI;
public bool IsCivFixed;
public bool IsHostControlled;
}
@ -2396,6 +2471,7 @@ namespace RuntimeData
GameArchiveManager.Instance.SaveQuickContinueRecord(Main.MapData);
AchievementDataManager.Instance.SaveAchievementData();
// 设置当前玩家
SyncSelfPlayerToLocalControl(nextPlayer.Id);
Main.PlayerLogic.StartPlayerTurn(this, nextPlayer.Id);
}
@ -2559,6 +2635,8 @@ namespace RuntimeData
public bool CheckIsRealPlayer(uint playerId)
{
if (playerId == 0) return false;
if (MapConfig != null && MapConfig.IsRealPlayerSlot(playerId)) return true;
if (Net.Mode == NetMode.Multi)
{
foreach (var kv in Net.Players)
@ -2573,16 +2651,77 @@ namespace RuntimeData
public bool CheckIsAI(uint playerId)
{
if (Net.Mode == NetMode.Multi)
{
foreach (var kv in Net.Players)
{
if (kv.Value == playerId) return false;
return !CheckIsRealPlayer(playerId);
}
public bool CanLocalControlPlayer(uint playerId)
{
if (playerId == 0) return false;
if (Net?.Mode == NetMode.Multi)
{
var lobby = LobbyManager.Instance.Lobby;
if (lobby == null || !lobby.IsInLobby()) return playerId == PlayerMap.SelfPlayerId;
var selfMemberId = lobby.GetSelfMemberId();
if (Net.Players.TryGetValue(selfMemberId, out var selfPlayerId) && selfPlayerId == playerId)
return true;
return lobby.IsLobbyOwner() && (MapConfig?.IsHostControlledPlayer(playerId) ?? false);
}
if (Net?.Mode == NetMode.Single)
return MapConfig?.IsRealPlayerSlot(playerId) ?? playerId == PlayerMap.SelfPlayerId;
return playerId == PlayerMap.SelfPlayerId;
}
public bool HasSurvivingLocalControlledPlayer()
{
if (PlayerMap?.PlayerDataList == null) return false;
foreach (var player in PlayerMap.PlayerDataList)
{
if (player == null || !player.IsSurvival) continue;
if (CanLocalControlPlayer(player.Id)) return true;
}
return false;
}
public bool SyncSelfPlayerToLocalControl(uint playerId)
{
if (!CanLocalControlPlayer(playerId)) return false;
if (PlayerMap == null || !PlayerMap.GetPlayerDataByPlayerID(playerId, out _)) return false;
if (PlayerMap.SelfPlayerId == playerId) return false;
PlayerMap.SelfPlayerId = playerId;
return true;
}
return playerId != PlayerMap.SelfPlayerId;
public bool SyncSelfPlayerToDefaultLocalControl()
{
if (PlayerMap?.PlayerDataList == null) return false;
if (TryGetLocalControlledPlayerId(preferSurvival: true, out var playerId)
|| TryGetLocalControlledPlayerId(preferSurvival: false, out playerId))
{
if (PlayerMap.SelfPlayerId == playerId) return false;
PlayerMap.SelfPlayerId = playerId;
return true;
}
return false;
}
private bool TryGetLocalControlledPlayerId(bool preferSurvival, out uint playerId)
{
playerId = 0;
if (PlayerMap?.PlayerDataList == null) return false;
foreach (var player in PlayerMap.PlayerDataList)
{
if (player == null) continue;
if (preferSurvival && !player.IsSurvival) continue;
if (!CanLocalControlPlayer(player.Id)) continue;
playerId = player.Id;
return true;
}
return false;
}
// 用于使用指定地图的地图相关内容重生成

View File

@ -98,9 +98,12 @@ namespace Logic
// 开始通用更新,主从客户端都走这一套
Main.MapData.RefreshTurn();
if (Main.MapData.CurPlayer == null) return;
var isLocalTurn = Main.MapData.CanLocalControlPlayer(Main.MapData.CurPlayer.Id);
if (isLocalTurn) Main.MapData.SyncSelfPlayerToLocalControl(Main.MapData.CurPlayer.Id);
else Main.MapData.SyncSelfPlayerToDefaultLocalControl();
if (_curState == GameState.Spectate) return;
else if (!Main.MapData.PlayerMap.SelfPlayerData.IsSurvival) ChangeState(GameState.DieWaiting);
else if (Main.MapData.CurPlayer.Id == Main.MapData.PlayerMap.SelfPlayerId) ChangeState(GameState.PlayerRound);
else if (!Main.MapData.HasSurvivingLocalControlledPlayer()) ChangeState(GameState.DieWaiting);
else if (isLocalTurn) ChangeState(GameState.PlayerRound);
else ChangeState(GameState.OtherPlayerRound);
UpdateAI();
UpdateConfirm();
@ -179,7 +182,7 @@ namespace Logic
private bool NeedAI()
{
if (Main.MapData.CurPlayer == Main.MapData.PlayerMap.SelfPlayerData) return false;
if (Main.MapData.CanLocalControlPlayer(Main.MapData.CurPlayer.Id)) return false;
var slot = Main.MapData.MapConfig?.MultiCivs?.Find(civ => civ != null && civ.PlayerId == Main.MapData.CurPlayer.Id);
if (slot != null && slot.MemberId != 0)

View File

@ -304,7 +304,7 @@ namespace Logic
if ((map.Net.Mode == NetMode.Multi || map.Net.Mode == NetMode.Spectator)
&& map.Net.Players.ContainsValue(playerId))
return;
if (map.PlayerMap.SelfPlayerData.Id == playerId) return;
if (!map.CheckIsAI(playerId)) return;
AIAddMoney(map, playerId);
}

View File

@ -151,6 +151,7 @@ namespace TH1_UI.View.Outside
private enum RoomMemberRowType
{
Human,
HostControlled,
Open,
Line,
AI
@ -472,8 +473,10 @@ namespace TH1_UI.View.Outside
{
_roomMemberRows.Clear();
var humanRows = new List<RoomMemberRowData>();
var hostControlledRows = new List<RoomMemberRowData>();
var openRows = new List<RoomMemberRowData>();
var emptyAiRows = new List<RoomMemberRowData>();
memberList.TryGetValue(_lobby.GetLobbyOwnerId(), out var ownerInfo);
for (int i = 0; i < multiCivs.Count; i++)
{
@ -487,6 +490,10 @@ namespace TH1_UI.View.Outside
{
emptyAiRows.Add(new RoomMemberRowData { Type = RoomMemberRowType.AI, MemberCiv = mc, SlotIndex = i });
}
else if (mc.MemberId == 0 && mc.IsHostControlled)
{
hostControlledRows.Add(new RoomMemberRowData { Type = RoomMemberRowType.HostControlled, MemberCiv = mc, MemberInfo = ownerInfo, SlotIndex = i });
}
else if (mc.MemberId == 0)
{
openRows.Add(new RoomMemberRowData { Type = RoomMemberRowType.Open, MemberCiv = mc, SlotIndex = i });
@ -494,12 +501,14 @@ namespace TH1_UI.View.Outside
}
int totalPlayerCount = Mathf.Max(0, (int)Main.Instance.MapConfig.PlayerCount);
int roomSeatCount = Mathf.Clamp(_roomSeatCount, humanRows.Count, Mathf.Min(totalPlayerCount, MaxRoomSeatCount));
_openMemberRowCount = Mathf.Max(0, roomSeatCount - humanRows.Count);
int occupiedSeatCount = humanRows.Count + hostControlledRows.Count;
int roomSeatCount = Mathf.Clamp(_roomSeatCount, occupiedSeatCount, Mathf.Min(totalPlayerCount, MaxRoomSeatCount));
_openMemberRowCount = Mathf.Max(0, roomSeatCount - occupiedSeatCount);
int aiCount = Mathf.Max(0, totalPlayerCount - roomSeatCount);
bool showAiRows = IsTeamAndAIConfigEnabled();
_roomMemberRows.AddRange(humanRows);
_roomMemberRows.AddRange(hostControlledRows);
for (int i = 0; i < _openMemberRowCount; i++)
{
if (i < openRows.Count)
@ -592,6 +601,24 @@ namespace TH1_UI.View.Outside
showTeamControls);
return;
}
if (row.Type == RoomMemberRowType.HostControlled)
{
if (row.MemberInfo != null)
{
memberRow.SetHumanContent(row.MemberInfo, Table.Instance.TextDataAssets.MultiplayRoomOwnerTitle, civ, force, teamId, maxTeamId, _lobby, _lobby.IsLobbyOwner(), forceNameOverride,
direction => OnOpenRowForceChanged(row.SlotIndex, direction),
direction => OnOpenRowTeamChanged(row.SlotIndex, direction),
showTeamControls);
}
else
{
memberRow.SetOpenContent(civ, force, teamId, maxTeamId, _lobby.IsLobbyOwner(), forceNameOverride,
direction => OnOpenRowForceChanged(row.SlotIndex, direction),
direction => OnOpenRowTeamChanged(row.SlotIndex, direction),
showTeamControls);
}
return;
}
var status = GetHumanStatus(mc);
bool canEditHuman = mc.MemberId == _lobby.GetSelfMemberId();
@ -815,7 +842,12 @@ namespace TH1_UI.View.Outside
_roomSeatCount = Mathf.Clamp(_roomSeatCount, _lobby.GetMemberCount(), maxRoomSeatCount);
int lobbyMemberLimit = _lobby.GetMemberLimit();
if (lobbyMemberLimit > 0) _roomSeatCount = Mathf.Clamp(lobbyMemberLimit, _lobby.GetMemberCount(), maxRoomSeatCount);
int openSlotBudget = Mathf.Max(0, _roomSeatCount - _lobby.GetMemberCount());
int hostControlledSeatCount = 0;
foreach (var mc in multiCivs)
{
if (mc != null && mc.MemberId == 0 && mc.IsHostControlled) hostControlledSeatCount++;
}
int openSlotBudget = Mathf.Max(0, _roomSeatCount - _lobby.GetMemberCount() - hostControlledSeatCount);
bool canReconcileOpenSlots = _lobby.IsLobbyOwner();
bool slotLayoutChanged = false;
var usedCivs = new HashSet<uint>();
@ -825,7 +857,7 @@ namespace TH1_UI.View.Outside
{
var mc = multiCivs[i];
if (mc == null) continue;
if (canReconcileOpenSlots && mc.MemberId == 0)
if (canReconcileOpenSlots && mc.MemberId == 0 && !mc.IsHostControlled)
{
bool shouldBeOpenSlot = openSlotBudget > 0;
if (mc.IsAI == shouldBeOpenSlot)