Family seat courtiers (Traders/Merchants, Patrons/Ministers) are cloned between players who found their family seat on the same turn. Both players receive courtiers with identical gender, name, ratings, archetype, and adjective traits.
Medium - Affects multiplayer balance and immersion when multiple players select the same family class.
All versions using the current addCourtier implementation.
The addCourtier function in Player.cs seeds its RNG using the game turn number instead of a unique identifier:
// Player.cs:21355-21374
protected virtual Character addCourtier(CourtierType eType, GenderType eGender, FamilyType eFamily, Player pFromPlayer = null)
{
RandomStruct pRandom = new RandomStruct(game().getSeedForId(game().getTurn()));
// ^^^^^^^^^^^^^^^
// BUG: Uses turn number, not character ID
if (eType == CourtierType.NONE)
{
eType = (CourtierType)(pRandom.Next((int)infos().courtiersNum()));
}
Character pCharacter = game().createNewCharacter(
infos().Globals.COURTIER_AGE,
getPlayer(),
eGender,
NameType.NONE,
eFamily,
TribeType.NONE,
((pFromPlayer != null) ? pFromPlayer.getNation() : NationType.NONE),
null,
pRandom.NextSeed() // Seed derived from turn-based RNG
);
pCharacter.setCourtier(eType);
pCharacter.generateRatingsCourtier(eType);
// ...
}Compare to other character creation functions that correctly use unique identifiers:
// Player.cs:15920 - createAdult
RandomStruct random = new RandomStruct(game().getSeedForId(game().getNextCharacterID()));
// Player.cs:16816 - character spawning
RandomStruct random = new RandomStruct(game().getSeedForId(game().getNextCharacterID()));
// Game.cs:10717 - child creation
RandomStruct random = new RandomStruct(getSeedForId(getNextCharacterID()));When addCourtier is called on Turn 0 for both players:
- Both players call
getSeedForId(game().getTurn())=getSeedForId(0) - Both receive identical seed S0
- Both call
pRandom.NextSeed()producing identical seed S1 - Both pass S1 to
createNewCharacter()
Inside createNewCharacter -> createCharacterNext:
RandomStruct random = new RandomStruct(S1)- same for both- Gender determined by
random.Next()- same result - Name determined by
randomFirstName(..., random.NextSeed())- same result - Character initialized with
random.NextSeed()- same seed
Inside generateRatingsCourtier:
- Ratings via
randomNext()- same results - Archetype via
nextRandomSeed()- same result - Adjective via
nextRandomSeed()- same result
Both courtiers are functionally identical clones, differing only in:
- Character ID (assigned via
getNextCharacterID()in createCharacter) - Player ownership
- Family assignment (if different families selected)
- Start a new multiplayer or hotseat game with 2 players
- Player 1 selects a nation with Traders as their first family
- Player 2 selects a nation with Traders as their first family
- Both players found their capital cities (family seats) on Turn 0
- Compare the Merchant courtiers granted to each player
Expected: Independent, randomly-generated courtiers Actual: Identical courtiers with same name, gender, ratings, and traits
| Family Class | Courtier Type | Bonus |
|---|---|---|
| Traders | Merchant | BONUS_FAMILYCLASS_TRADERS_SEAT |
| Patrons | Minister | BONUS_FAMILYCLASS_PATRONS_SEAT |
Replace the turn-based seed with a character-ID-based seed, consistent with other character creation code:
protected virtual Character addCourtier(CourtierType eType, GenderType eGender, FamilyType eFamily, Player pFromPlayer = null)
{
// FIX: Use getNextCharacterID() instead of getTurn()
RandomStruct pRandom = new RandomStruct(game().getSeedForId(game().getNextCharacterID()));
// ... rest of function unchanged
}This ensures each courtier receives a unique seed regardless of when they are created.
If the original intent was to make courtier creation deterministic based on game state (for replay consistency), an alternative approach would be to incorporate additional unique factors:
// Option: Combine turn with player ID for uniqueness while maintaining determinism
RandomStruct pRandom = new RandomStruct(game().getSeedForId(game().getTurn() * 1000 + (int)getPlayer()));However, the standard approach using getNextCharacterID() is recommended for consistency with the rest of the codebase.
Source/Base/Game/GameCore/Player.cs:21355-21374-addCourtier()(bug location)Source/Base/Game/GameCore/Player.cs:15920-createAdult()(correct pattern)Source/Base/Game/GameCore/Player.cs:16816- character spawning (correct pattern)Source/Base/Game/GameCore/Game.cs:10309-10338-createCharacterNext()Source/Base/Game/GameCore/Game.cs:10340-10343-createNewCharacter()Source/Base/Game/GameCore/Character.cs:8805-8841-generateRatingsCourtier()Source/Base/Game/GameCore/PlayerBonus.cs:5861-5870- bonus processing that callsaddCourtier()