Skip to content

CHANGELOG

v0.54.0 (2026-10-12)

Bug Fixes

  • accounts: Empire_core.accounts names the module (e791ea2)

  • gamedata: One source for the loaded game data (eda0f08)

  • services: One spend_rubies consent for every call that can spend rubies (d6ce8be)

Documentation

  • Check for None and read typed fields in the examples (dc84806)

Features

  • One import home per enum and one object per public name (124c230)

  • events: Get_event is typed per event (2bdf32f)

  • gamedata: Load the game data on first use (f90ae39)

  • protocol: Models take their field names under every type checker (713266c)

  • utils: Event registration returns the callback (492aa8b)

Refactoring

  • protocol: Declare each wire key once, on one model base (e5c6b58)

Breaking Changes

  • Empire_core.commanders.General (the owned general model, GetGeneralsResponse.generals) is renamed OwnedGeneral; the name General is now only the generated id enum (empire_core.gamedata, empire_core.enums). empire_core.Building, empire_core.state.Building and Castle.buildings are removed: they were never filled; read a castle's buildings from client.castle.join. GGEError, VersionCheckStatus, TargetRead and EffectTemplate are defined in empire_core.enums; import them from a public path (empire_core.enums, or protocol, exceptions, combat and gamedata as before) instead of their old private modules.

  • accounts: The shared registry is empire_core.account_registry (also empire_core.accounts.account_registry); empire_core.accounts is the module. Replace from empire_core import accounts with from empire_core import account_registry.

  • gamedata: AllianceChronicleEntry.describe() on a daimyo contract entry without game_data no longer raises ValueError: it loads the game data, and raises GameDataNotLoadedError when that fails. Catch GameDataNotLoadedError instead of ValueError.

  • protocol: Building a model by wire key (AttackWave(L=...)) is now a type error under pyright and Pylance too (mypy already rejected it); it still works at runtime. Build by field name (AttackWave(left=...)) or parse with model_validate. FieldInfo.alias is None for every wire field; read serialization_alias (or validation_alias) for the wire key.

  • services: Pay_with_rubies is renamed spend_rubies on army.produce_units and castle.build, upgrade_building, upgrade_defense and repair_building; in produce_units it is now keyword-only, with private_offer_id and timeout. castle.upgrade_building, upgrade_defense and finish_construction take castle_id first, (castle_id, object_id, *, ...), and join that castle (jca) on every call, which makes it the session's active castle. castle.finish_construction loses free_skip: it sends the free finish when the time left it works out is 240 s or less, and otherwise, or when the castle, the object, its building or upgrade target, the player level or the state does not let it work the time out (a repair or tear-down), needs spend_rubies=True; it needs client.load_game_data(). BuildingRow.construction_seconds_left is renamed construction_completed_seconds, the seconds done, as the client reads it. These now raise ValueError before sending unless spend_rubies=True: army.double_production_slot, army.skip_heal and castle.repair_all always; army.heal_all for a ruby_cost above 0; army.heal_units and army.produce_units for a unit priced in rubies (or not in the game data); castle.build for a building priced in rubies; castle.upgrade_building and upgrade_defense when the next level is priced in rubies at your level, or the object or its next level is not known; castle.buy_expansion for ExpansionType.PREMIUM; castle.rename unless is_initial_name or a premium account runs; alliance.donate when rubies are donated; castle.send_resources, send_support, send_troops, attack.send_attack and spy.send_spy_mission, send_instant_spy, execute_instant_spy and send_sabotage for a horse priced in rubies without feathers or any slowdown. Pricing a unit, building or horse needs client.load_game_data() or raises GameDataNotLoadedError. timeout is keyword-only on double_production_slot, heal_units, heal_all, skip_heal, repair_all, buy_expansion and donate. Pass spend_rubies=True where spending is meant.

v0.53.0 (2026-10-12)

Bug Fixes

  • client: Retry a version refusal when logging in with the live version (730a45c)

  • config: Log in with the live client's version (1e1c5e2)

  • map: Pace map area reads under the server's tile budget (9658217)

  • map: Pace map reads at 25,000 tiles a second (6346d56)

Features

  • gamedata: Regenerate ids for items v787.03 (d920b3d)

Testing

  • client: Stub the live version fetch with a function mypy accepts (9a8b30e)

  • map: Stub the pacer wait with a function mypy accepts (875309c)

Breaking Changes

  • config: EmpireConfig.client_version defaults to None instead of "1.169.11", and the EmpireConfig.build_number property is removed; use empire_core.config.build_number(version).

v0.52.0 (2026-10-12)

Bug Fixes

  • accounts: Say why no account could be leased (#438, 5d588e5)

Breaking Changes

  • accounts: AccountRegistry.load raises FileNotFoundError for a missing explicit file_path and ValueError for a file that is not a JSON list, where it logged and loaded nothing; pass no file_path to make the file optional. The file_path default is None (meaning accounts.json in the current directory, if present) instead of "accounts.json".

v0.51.2 (2026-10-11)

Bug Fixes

  • accounts: Log in to an account's world on that world's server (#436, c3368a1)

v0.51.1 (2026-10-09)

Bug Fixes

  • accounts: Read accounts.json as UTF-8 on every platform (#398, 9921b30)

v0.51.0 (2026-10-09)

Bug Fixes

  • castle: Check the minute skip before skip_construction_time sends it (b54f9c3)

Features

  • alliance: Type a chronicle entry's values by action and write its line (5489ed0)

  • castle: Ask for the travel pre-calculation before sending troops (b22c550)

  • castle: Transfer goods to another kingdom, and minute-skip a transfer (3a19228)

  • player: List, start and collect mercenary camp missions (adfbcd7)

  • player: Start research and skip it with a minute skip (cfeed56)

  • state: Apply every grc block to its castle's resources (f41f51b)

Testing

  • Drop an account name from two login bonus fixtures (bea44a6)

Breaking Changes

  • castle: Client.castle.skip_construction_time raises ValueError for a currency that is no minute skip, or a minute skip you hold none of, where it sent msb before; pass a Currency.SKIP_* you hold.

v0.50.0 (2026-10-08)

Bug Fixes

  • alliance,commanders,attack: Read the crest colours and skill ids as the client does (c2d1ac5)

  • gamedata,castle: Read short slot pairs, text tool ids and a constructed slot list as the client does (8a63979)

  • gamedata,movements: Name gem and crest layout collectables, merge as the client merges, guard the cache (b38ce85)

  • map: Collect main castles in every kingdom by default (1de62ff)

  • protocol: Say when an id array has entries that are not ids (4ae50e4)

  • scripts: Generate a new id table from a clean package (0b96310)

Features

  • army: Build a flank and a preset from units and tools by amount (f17cb09)

  • army: Type army slots and inventories as units and tools (c148f48)

  • castle: Type castle inventories, movement armies and spy armies (5327342)

  • events,gamedata: A campaign's and a title's rewards as collectables (cf405e4)

  • gamedata: Enums for the tables the game names by text (7cf8eca)

  • gamedata: Key every id-enum table by its enum (5f77fbe)

  • gamedata: Name currencies and researches from the game's texts (3568a9f)

  • gamedata: Parse effects columns into typed effect values (c50f619)

  • gamedata: Read the rewards table into collectables (5912f24)

  • gamedata: Type the fixed text columns as enums (e988705)

  • gamedata: Type the ids game-data rows reference (09a6d4a)

  • gamedata: Type the tables the id enums name (6337193)

  • map,ranking: Type the battle ground connections and the highscore rows (5d6de8c)

  • messages: Type a message's type as MessageType (f83665c)

  • messages,commanders,player: Type the advisor, premium and title codes and the small rows (5716903)

  • models: Name the title, gem, sceat skill and horse ids the enums now cover (73e88c2)

  • models: Read goods, loot and currency rows as collectables (92ddacc)

  • models: Type the id lists the generated enums now name (65cb9ba)

  • movements: Read a movement's area rows as map rows and type its advisor and auto-skip (a6401dc)

  • texts: Display names and effect descriptions from the game's texts (6da3aa1)

  • texts: Fill a text's arguments as the castle client does (2cae7b9)

  • texts: One loader for the game's language file (c25ee9d)

Performance Improvements

  • gamedata: Read the typed tables lazily (ecb6867)

Testing

  • commanders: Give the gie fixture real general skill ids (3d26fb2)

Breaking Changes

  • alliance,commanders,attack: CrestLayout.colors is renamed color_ids and is a tuple of AllianceCrestColor (or the int sent) or None, not the list as sent. protocol.base.int_entries takes the keyword-only warn (a logger) and what (the entries' name, for its warning) since 83de959f, whose message had no footer for it: call int_entries(value, warn=logger, what="...").

  • army: WaveFlank.units/tools, PresetArmy._units/_tools, CreateAttackRequest.yard_wave, FilledAttack.yard, fill_yard_wave(), the defense slot fields (WallSection, KeepDefense, MoatDefense and the dfk/dfw/dfm requests) and SendSupportRequest/SendTroopsRequest/ KingdomUnitTransferRequest.units are tuples of WodAmount, not [[wod_id, amount]] lists; build them with WodAmount.slots({...}) or WodAmount(item, amount), and an empty slot is EMPTY_SLOT (WodAmount(None, 0)), not [-1, 0]. PresetArmy.support_tools and CreateAttackRequest.support_tools hold None for an empty slot, not -1 (PresetArmy's default is (None, None, None)). CreateAttackRequest. collector_booster and send_attack(collector_booster=) are {CurrencyId: amount}, not [[currency_id, amount]]. send_attack( yard_wave=), send_support/send_troops/transfer_units_to_kingdom(units=) take a mapping such as {Unit.SWORDMAN: 100} or WodAmount pairs, not [[wod_id, amount]] lists; wave_limit_violations(yard=) takes WodAmount pairs. protocol.UnitCount is removed: army.get_units() returns {Unit or Tool: amount}, and GetUnitsResponse.get_inventory/ get_in_production/get_stronghold/get_hospital are removed, read .units/.in_production/.stronghold/.hospital instead. wod_amount_pairs moved from empire_core.army to empire_core.gamedata. The gui inventories, GetDefenseResponse.inventory(), Castle.units and Movement.units are keyed by Unit/Tool members (IntEnums, so an int id still looks them up); UnitsReceived.wod_id is a Unit/Tool member; Player.beginner_protection is keyed by Kingdom; GameData.fortifications is keyed by Building. LenientEnum.enum is replaced by LenientEnum.enums.

  • castle: DetailedCastleInfo.raw_units, raw_stronghold_units, raw_hospital_units and raw_travelling_units are removed; units, stronghold_units, hospital_units and travelling_units are now fields, {Unit or Tool: amount}, and an id sent twice adds up. MovementArmy.left/ middle/right/courtyard and MovementWrapper.travel_units are {Unit or Tool: amount}, not [[wod_id, amount]] lists. MovementWrapper. support_tools holds None for an empty slot; Movement.support_tool_ids is renamed Movement.support_tools, of the same type. AttackInfoResponse. spy_data and spy_army() are replaced by the field spy_army, and SpyReportResponse.spy_data and army() by the field army, each a SpyArmy or None. GetSupportDefenseResponse.defense_positions is a SpyArmy or None; get_total_defenders() and get_units_by_position() are removed, use defense_positions.total() and .sections(). SpyArmy is a pydantic model: SpyArmy.from_spy_data(S) is SpyArmy.model_validate(S), its positions are tuples of WodAmount, and army.UnitStack is removed (.wod_id/.count are WodAmount's .item/.amount). CastleUnitUnlocks. unlocked_unit_ids/locked_unit_ids are renamed unlocked_units/ locked_units, tuples of Unit or Tool. GetDefenseResponse.range_priority/ melee_priority are tuples of Unit. BuildingRow.raw_data is removed. ConstructionList.object_ids is replaced by slots, a tuple of ConstructionSlot (position, object_id, is_waiting, is_free, is_locked).

  • gamedata: CampaignEvent.reward_ids is tuple[RewardId, ...] and TitleDef.reward_id is a RewardId (both still ints at runtime); look their collectables up with GameData.reward_list. The game data cache format changed: existing caches are re-parsed once. Table() takes a context.

  • gamedata: EffectDef.raw_area_type_ids, raw_space_ids and raw_raid_boss_ids, EquipmentEffectDef.raw_item_group_ids and ToolStats.raw_slot_types are removed: read area_type_ids, space_ids, raid_boss_ids, item_group_ids and slot_types, now tuple fields of enum members (slot types of ints). EffectDef.effect_type_id, RelicEffectDef.effect_id, EquipmentEffectDef.effect_id and wearer_id, DefaultLordDef.wearer_id, GeneralSkillDef.general_id, DungeonDefence.kingdom_id, LeagueBracketDef.event_id and FortificationDef.wod_id hold enum members (ints) instead of plain ints.

  • gamedata: GameData.buildings, researches, events, loot_boxes, loot_box_types, equipment_groups, difficulty_types, quests, daily_quests, titles and scaling_camps are read-only Table mappings (exported from empire_core.gamedata) instead of dicts, and are no longer GameData fields: GameData(buildings=...) and item assignment are gone; build a GameData with GameData.parse. Reading is unchanged (table[id], .get(), in, .items(), len()).

  • gamedata: GameData.gems, GameData.sceat_skills and GameData.horses are read-only Table mappings keyed by Gem, SceatSkill and Horse instead of dict[int, ...] fields: GameData(gems=...) and item assignment are gone, build with GameData.parse. GameData.titles is keyed by Title. GemDef.gem_id, SceatSkillDef.skill_id, HorseStats.wod_id, TitleDef.title_id and previous_title_id and BuildingDef.sceat_skill_id hold the enum member (an int) instead of a plain int. The cache file moves these tables under "table_rows".

  • gamedata: GameData.record(), GameData.records(), IdRecord, empire_core.gamedata.data.rows_by_id and ROW_TABLES are removed; index the table with the member instead (game_data.units[Unit.MEAD_RANGER_L6], game_data.buildings[Building.KEEP_L1]), a comprehension for several. GameData.buildings, researches, events, loot_boxes, loot_box_types, equipment_groups, difficulty_types, quests, daily_quests and titles hold BuildingDef, ResearchDef, EventDef, LootBoxDef, LootBoxTypeDef, EquipmentGroupDef, DifficultyTypeDef, QuestDef, DailyQuestDef and TitleDef instead of raw dicts: read fields (row.raw_effects, row.previous_title_id) instead of row.get("effects"). GameData.raw("eventAutoScalingCamps") and RAW_TABLES no longer have that table; use GameData.scaling_camps.

  • gamedata: GameData.units, tools, effects, effect_types, construction_items, global_effects, general_skills, legend_skills, generals, general_abilities, currencies and raid_bosses are read-only Table mappings keyed by their id enum instead of dict[int, ...] fields: GameData(units=...) and item assignment are gone, build with GameData.parse. UnitStats.wod_id, ToolStats.wod_id, EffectDef.effect_id, EffectTypeDef.effect_type_id, ConstructionItemDef.construction_item_id, GlobalEffectDef.global_effect_id, GeneralSkillDef.skill_id, LegendSkillDef.skill_id, GeneralDef.general_id, GeneralAbilityDef.ability_id, CurrencyDef.currency_id and RaidBossDef.raid_boss_id hold the enum member (an int) instead of a plain int. The cache file moves these tables under "table_rows".

  • gamedata: GameData.units_by_role(role) takes a UnitRole (UnitRole.MELEE, UnitRole.RANGED) instead of a str. UnitStats.role, ToolStats.category and tool_category, BuildingDef.group and building_ground_type, QuestCondition.condition_type, TitleDef.title_system and display_type, EffectDef.player_relation and RelicEffectDef.relic_effect_type now hold their enum's member (a str subclass, so == "melee" still holds), or the plain text for a value the enum lacks; compare against the members.

  • gamedata: Raw_effects is removed from ToolStats, EffectSpecRow (ConstructionItemDef, AllianceBuffDef, GlobalEffectDef, SceatSkillDef, GeneralSkillDef, GemDef), EquipmentSetDef, DefaultLordDef, BuildingDef, ResearchDef and TitleDef, and BuildingDef.raw_area_specific_effects is now area_specific_effects: read row.effects, a tuple of EffectValue (EquipmentEffectValue for equipment sets and default commanders). EquipmentSetDef and DefaultLordDef now derive from EquipmentEffectSpecRow instead of EffectSpecRow. empire_core.combat.parse_effect_spec(spec) is removed; use effect_value_bonuses(row.effects).

  • gamedata: SCEItem is removed; use the generated Currency (from empire_core.gamedata), same values and names, except: FEATHERS -> FAST_TRAVEL_FEATHERS, SKIP_1_MIN -> SKIP_1_MINUTE, SKIP_5_MIN -> SKIP_5_MINUTES, SKIP_10_MIN -> SKIP_10_MINUTES, SKIP_30_MIN -> SKIP_30_MINUTES, SKIP_1_HR -> SKIP_1_HOUR, SKIP_5_HRS -> SKIP_5_HOURS, SKIP_24_HRS -> SKIP_24_HOURS, SCEATS -> SCEAT, CONSTRUCTION_TOKENS -> CONSTRUCTION_TOKEN, UPGRADE_TOKENS -> UPGRADE_TOKEN, RELIC_SHARDS -> RELIC_SPLINTERS, ALLIANCE_COINS -> ALLIANCE_COIN, RIFT_COINS -> RIFT_COIN, PASSAGE_TOKENS -> CASTLE_PASSAGE_TOKEN, BUILD_BOOSTER_RARE -> RARE_BUILD_ITEM_BOOSTER, BUILD_BOOSTER_EPIC -> EPIC_BUILD_ITEM_BOOSTER, BUILD_BOOSTER_LEGENDARY -> LEGENDARY_BUILD_ITEM_BOOSTER, GENERAL_SKILL_RESET -> GENERALS_SKILL_RESET_TOKEN, GENERAL_XP_5K -> GENERALS_XP_5000, GENERAL_XP_10K -> GENERALS_XP_10000, GENERAL_XP_15K -> GENERALS_XP_15000, OFFERING_LUDWIG -> OFFERING_TO_LUDWIG, OFFERING_ULRICH -> OFFERING_TO_ULRICH, OFFERING_BEATRICE -> OFFERING_TO_BEATRICE, OFFERING_SASAKI -> SASAKIS_OFFERING_TO_BEATRICE, OFFERING_TIZI -> TIZIS_OFFERING_TO_ULRICH, OFFERING_HASAN -> HASANS_OFFERING_TO_LUDWIG, OFFERING_DIANA -> DIANAS_OFFERING_TO_ULRICH, OFFERING_ASHIRA -> ASHIRAS_OFFERING_TO_ULRICH, OFFERING_KAELRITH -> KAELRITHS_OFFERING_TO_BEATRICE, OFFERING_BARIN -> BARINS_OFFERING_TO_ULRICH, OFFERING_EDRIC -> EDRICS_OFFERING_TO_LUDWIG, SHARD_TORIL -> TORILS_GENERAL_SHARD, SHARD_LEO -> LEOS_GENERAL_SHARD, SHARD_ALYSSA -> ALYSSAS_GENERAL_SHARD, SHARD_HORATIO -> HORATIOS_GENERAL_SHARD, SHARD_SASAKI -> SASAKIS_GENERAL_SHARD, SHARD_DIANA -> DIANAS_GENERAL_SHARD, SHARD_TOM -> TOMS_GENERAL_SHARD, SHARD_TIZI -> TIZIS_GENERAL_SHARD, SHARD_HASAN -> HASANS_GENERAL_SHARD, SHARD_GARRIK -> GARRIKS_GENERAL_SHARD, SHARD_KAELRITH -> KAELRITHS_GENERAL_SHARD. BREAKING CHANGE: TimeSkipBuildingRequest.minute_skip, MinuteSkipDungeonRequest.minute_skip and castle.skip_construction_time(minute_skip) take a Currency (a key str still works); Player.special_currencies and state.get_special_currencies() are keyed by Currency, a key the enum lacks kept as its str. BREAKING CHANGE: Currency and CurrencyId members are renamed from the server key to the game's English name (values unchanged): AC -> ALLIANCE_COIN, ACO -> ANNIVERSARY_TABLETS, AFT -> ADVISOR_CONTRACT, AIN -> ALLIANCE_INFLUENCE, APT -> APPRENTICE_TOKENS, AS -> ALLIANCE_STATUETTES, AST -> ASHIRAS_OFFERING_TO_ULRICH, BA -> PARTY_BALLOONS, BAB -> BALLOON_BOOSTER, BAT -> BARON_ADVISOR_TOKEN, BC1 -> RARE_BUILD_ITEM_BOOSTER, BC2 -> EPIC_BUILD_ITEM_BOOSTER, BC3 -> LEGENDARY_BUILD_ITEM_BOOSTER, BD -> BASTION_DOUBLOON, BE -> BEADS, BEB -> BEAD_BOOSTER, BEE -> BEES, BERAT -> CHRONICLERS_TOKEN, BRN -> BARINS_OFFERING_TO_ULRICH, BTO -> COPPER_PIECES, BUB -> BUSY_BEES, CC -> FROZEN_CARROT, CMBK -> COMMON_MYSTERY_KEY, CMK -> CARNIVAL_MASK, CO1 -> SCREWS, CO2 -> BLACK_POWDER, CO3 -> SAW, CO4 -> DRILL, CO5 -> CROWBAR, CO6 -> LEATHER_STRIPS, CO7 -> CHAINS, CO8 -> METAL_PLATES, CPT -> CASTLE_PASSAGE_TOKEN, DAT -> DIANAS_OFFERING_TO_ULRICH, DC -> DRAGON_CHARMS, DD -> DECORATION_DUST, DDP -> DUTY_POINTS, DG -> DRAGON_GLASS, DGA -> DRAGON_GLASS_ARROWS, DRPT -> FIRE_WYRM_TOKENS, DSAM -> DRAGON_SCALE_ARMOR, DSAW -> DRAGON_SCALE_ARROWS, DSS -> DRAGON_SCALE_SPLINTERS, DST -> DRAGON_SCALE_TILES, EDR -> EDRICS_OFFERING_TO_LUDWIG, EMBK -> EPIC_MYSTERY_KEY, ES -> ESSENCES, ESB -> ESSENCE_BOOSTER, FC -> FUSION_COINS, FD -> FURY_DOUBLOON, FKT -> OFFERING_TO_LUDWIG, FT -> FLORA_TOKENS, FU -> PELTS, FUB -> HUNTING_BOOSTER, GAT -> GARRIKS_OFFERING_TO_LUDWIG, GRT -> GENERALS_SKILL_RESET_TOKEN, GTO -> GOLD_PIECES, GTS -> THORN_TALISMANS, GXP1 -> GENERALS_XP_250, GXP2 -> GENERALS_XP_500, GXP3 -> GENERALS_XP_1000, GXP4 -> GENERALS_XP_2500, GXP5 -> GENERALS_XP_5000, GXP6 -> GENERALS_XP_7500, GXP7 -> GENERALS_XP_10000, GXP8 -> GENERALS_XP_12500, GXP9 -> GENERALS_XP_15000, HAT -> HASANS_OFFERING_TO_LUDWIG, HE -> HERITAGE, HEB -> HERITAGE_BOOSTER, HME -> HOLLOW_MOON_EGG, HWT -> SPOOKY_TOKEN, IAP -> CARGO_POINTS, IDCT -> IMPERIAL_DUCATS, IN -> INFLUENCE, INB -> INFLUENCE_BOOSTER, IPC -> IMPERIAL_PATRONAGE_CHARTER, IT -> GLACIER_TOKEN, KLT -> KAELRITHS_OFFERING_TO_BEATRICE, KM -> KHAN_MEDALS, KT -> KHAN_TABLETS, KTK -> OFFERING_TO_ULRICH, LCS -> LUCKY_SLICE, LFT -> LOTUS_TOKEN, LM -> UPGRADE_TOKEN, LMBK -> LEGENDARY_MYSTERY_KEY, LRC -> LEGENDARY_RIFT_COIN, LT -> CONSTRUCTION_TOKEN, LWT -> TICKETS, MCB -> BRICKS, MCC -> CLAY, MCF -> FINE_SAND, MCP -> PURIFIED_WATER, MCS -> STRAW, MCT -> TIMBER, MEG -> COBBLESTONE, MEP -> PRECIOUS_METALS, MER -> RESIN, MFT -> FARMING_TOOLS, MK -> MERCHANTS_KEY, MLF -> FABRIC, MLM -> MAGMA_STONE, MLS -> SOUL_STONE, MPT -> SUNRISE_TOKENS, MRF -> FLINT, MRG -> GLUE, MRN -> NAILS, MRP -> PAINT, MRR -> ROPE, MS1 -> SKIP_1_MINUTE, MS2 -> SKIP_5_MINUTES, MS3 -> SKIP_10_MINUTES, MS4 -> SKIP_30_MINUTES, MS5 -> SKIP_1_HOUR, MS6 -> SKIP_5_HOURS, MS7 -> SKIP_24_HOURS, NAT -> PATHFINDERS_SEAL, NKT -> MAJESTIC_TOKEN, OFS -> OFFERING_SHARD, OFT -> OKTOBERFEST_TOKEN, ORG -> WISHING_ORANGE, PL -> PLASTER, PPT -> JOLLY_ROGER_TOKENS, PR -> PEARLS, PTK -> OFFERING_TO_BEATRICE, PTT -> FAST_TRAVEL_FEATHERS, QT -> QUEST_TICKET, RC -> RIFT_COIN, RD -> RAMPART_DOUBLOON, RF -> RELIC_SPLINTERS, RL -> REFINED_WOOD, RMBK -> RARE_MYSTERY_KEY, RP -> RIFT_POINT, RS -> REFINED_STONE, RSH -> RIFT_SHARD, RVT -> VILLAGE_TOKENS, SAI -> ALICES_GENERAL_SHARD, SAL -> ALYSSAS_GENERAL_SHARD, SAS -> ASHIRAS_GENERAL_SHARD, SAT -> DAICHIS_SEAL, SB -> SOLDIER_BISCUIT, SBR -> BARINS_GENERAL_SHARD, SD -> SPIRIT_DOUBLOON, SDN -> DIANAS_GENERAL_SHARD, SDR -> EDRICS_GENERAL_SHARD, SGA -> GARRIKS_GENERAL_SHARD, SHS -> HASANS_GENERAL_SHARD, SHT -> HORATIOS_GENERAL_SHARD, SKL -> KAELRITHS_GENERAL_SHARD, SLE -> LEOS_GENERAL_SHARD, SLWT -> AFFLUENCE_TICKETS, SM -> SAMURAI_MEDALS, SMA -> STATUETTE_PENALTY, SMB -> SAMURAI_MEDAL_BOOSTER, SOB -> SOUL_SNATCHER, SOM -> TOMS_GENERAL_SHARD, SOUL -> SOULS, SP -> SHOGUN_POINTS, SPB -> SHOGUN_POINTS_BOOSTER, SPT -> SPRING_BLOSSOMS, SSK -> SASAKIS_GENERAL_SHARD, ST -> SAMURAI_TOKENS, STA -> STATUETTE, STAB -> STATUETTE_BOOSTER, STK -> SASAKIS_OFFERING_TO_BEATRICE, STL -> TORILS_GENERAL_SHARD, STO -> SILVER_PIECES, STP -> SCEAT, STPT -> LUCKY_GOLD_POT, STT -> TIZIS_GENERAL_SHARD, SUN -> UNIVERSAL_GENERAL_SHARD, SVA -> VALENTAS_GENERAL_SHARD, SWT -> SWEET_TREAT, TAT -> TOMS_AND_ALICES_OFFERING_TO_LUDWIG, TD -> TIME_DOUBLOON, TEA -> TEAPOTS, TEAT -> TEA_TIME_BOOSTER, TFA -> TWIN_FLAME_AXES, TO -> TONIC, TOB -> TONIC_BOOSTER, TS -> TALISMANS, TTK -> TIZIS_OFFERING_TO_ULRICH, UGR -> RUNE_GOLD, USR -> RUNESTONE, VAT -> VALENTAS_OFFERING_TO_LUDWIG, VD -> VIGOR_DOUBLOON, WWC -> LUCKY_PENNIES, XST -> WINTERTIDE_CHARMS. BREAKING CHANGE: Research members are renamed from the comment2 note and group to the game's title (values unchanged; _L as before): ADD_PLUNDER_CAPACITY_G48_L* -> HEAVY_DUTY_PACK_L*, AGENTENANZAHL_G21_L* -> GUTTER_RUNNERS_L*, AMOUNT_BOOST_PEASANTS_G32_L* -> CONSCRIPTION_L*, DECREASE_FORGING_COSTS_G34_L* -> ARTISAN_BLACKSMITH_L*, DECREASE_FORGING_COSTS_G44_L* -> FURNACE_L*, ELITEBOWMAN_G6_L* -> VETERAN_BOWMAN_L*, ELITECROSSBOW_G8_L* -> VETERAN_CROSSBOWMAN_L*, ELITEHALBERD_G37_L* -> VETERAN_HALBERDIER_L*, ELITEHEAVYCROSSBOWMAN_G40_L* -> VETERAN_HEAVY_CROSSBOWMAN_L*, ELITELONGBOWMAN_G38_L* -> VETERAN_LONGBOWMAN_L*, ELITEMACE_G7_L* -> VETERAN_MACEMAN_L*, ELITESPEER_G5_L* -> VETERAN_SPEARMAN_L*, ELITESWORDMAN_G29_L* -> VETERAN_SWORDSMAN_L*, ELITETWOHANDEDSWORD_G39_L* -> VETERAN_TWO_HANDED_SWORDSMAN_L*, ELITE_SCHNELLER_G17_L* -> ELITE_TRAINING_L*, FAME_BOOST_G24_L* -> MINSTREL_L*, FAME_BOOST_G53_L* -> PAMPHLETS_L*, FIREBRIGADE_BOOST_G36_L* -> FIRE_PUMP_L*, FOODBOOST_G14_L* -> THREE_FIELD_CROP_ROTATION_L*, GEB_UDEXP_G16_L* -> WHEELBARROW_L*, HOLZBOOST_G12_L* -> PITSAW_L*, HONEYBOOST_G139_L* -> HONEY_POTS_L*, HONOR_BOOST_G25_L* -> BIOGRAPHER_L*, INCREASE_POPULATION_G43_L* -> BUNK_BEDS_L*, KAMPFXP_G3_L* -> CHIVALRY_L*, KARRENGESCHW_G11_L* -> STEEL_TIRES_L*, KARRENKAPAZIT_T_G19_L* -> REINFORCED_AXLE_L*, MAGIC_FIND_BOOST_G28_L* -> TREASURE_SEEKER_L*, MAGIC_FIND_BOOST_G54_L* -> APPRAISER_L*, MEADBOOST_G140_L* -> NORTHMAN_TANKARDS_L*, MILITIA_ELITE_PEASANT_G30_L* -> MILITIA_L*, PERCEPTION_BOOST_SIGHTRANGE_G26_L* -> SCOUT_L*, PL_NDERBONUS_G4_L* -> LOOT_LOG_L*, POPULATION_BOOST_G35_L* -> MEDICINE_L*, PRODUCTION_SPEED_TOOLS_G42_L* -> CRAFTSMANSHIP_L*, PRODUCTIVITY_FOOD_G47_L* -> PLOW_L*, PRODUCTIVITY_STONE_G46_L* -> PICKAXE_L*, PRODUCTIVITY_WOOD_G45_L* -> TREE_NURSERY_L*, RECRUITMENT_SPEED_G41_L* -> STRENGTH_TRAINING_L*, REISEZEIT_KINGDOMS_RESSOURCEN_G23_L* -> CARAVAN_L*, REISEZEIT_KINGDOMS_SOLDATEN_G22_L* -> MARCHING_FORMATION_L*, REKRUTIERUNGSGESCHW_G1_L* -> MANEUVER_L*, RELICAXE_G201_L* -> RELIC_AXEMAN_UPGRADE_L*, RELIC_HAMMER_G202_L* -> RELIC_HAMMERMAN_UPGRADE_L*, RELIC_LONGBOW_G204_L* -> RELIC_LONGBOWMAN_UPGRADE_L*, RELIC_SHORTBOW_G203_L* -> RELIC_SHORTBOWMAN_UPGRADE_L*, RESEARCH_SPEED_BOOST_G33_L* -> GENIUS_L*, RESEARCH_SPEED_BOOST_G49_L* -> THIRST_FOR_KNOWLEDGE_L*, R_12H_STEUEREINTREIBER_G18_L* -> CASH_TRANSPORT_L*, SPIONGESCHW_G10_L* -> SHADY_BUSINESS_L*, STATIONIERUNGSGESCHW_G9_L* -> FORCED_MARCH_L*, STEALTH_BOOST_SIGHTRANGE_G27_L* -> RANGER_L*, STEINBOOST_G13_L* -> WEDGING_L*, STEUERBONUS_G15_L* -> LEVIES_L*, SURVIVOR_RATE_BOOST_G31_L* -> AMBULANCE_COACH_L*, TAX_COLLECTOR_BOOST_G52_L* -> INTEREST_RATES_L*, TOOLGESCHW_G2_L* -> WORKBENCH_L*, TRAVELING_SPEED_TRADING_BARROWS_G51_L* -> POWER_FOOD_L*, WACHENANZAHL_G20_L* -> WORKING_SHIFTS_L*, XP_BOOST_G50_L* -> SHIELD_BEARERS_L*. Refs #320 Refs #328

  • gamedata,castle: WodAmount.slots() returns WodAmount pairs for [wod_id, amount] lists too (read through int(), -1 an empty slot), where it kept them as given. Corrections to the footers of 5c9060cf and db39367a, whose behavior is unchanged here: SpyArmy.from_spy_data is removed, not renamed; use SpyArmy.model_validate(S), which raises on an S that is not a list or object (from_spy_data returned None) and reads an object by field name, so {} is an empty army. Movement.units drops amounts of 0 or less within each flank before the flanks are added up, where it added the raw pairs.

  • gamedata,movements: Collectable.item is a Gem for GEM and an AllianceCrestLayout for ALLIANCE_CREST_LAYOUT (or the int sent), not the plain int. Collectable.merged (and GameData.reward_list(combine=True)) adds up every kind the client adds up: loot boxes by kind into the first box, resource and achievement points and the like by kind, crest layouts by layout with their durations added, VIP time and dungeon protection by their durations. It no longer adds up any kind the client does not. Behavior changes from cd78a04b without a footer there: MovementArea.name of a village is the name at row index 8 (it was always empty), and Movement.target_area_id/target_name (and the source ones) of an area type the old table did not list, such as a faction camp, now carry the row's object id, owner and name instead of -1 and "". Movement.resources (since 11a09458) counts the goods of an old-style goods list, which it dropped before. Game data caches written before this commit are re-parsed once.

  • map,ranking: MapAreaItem.abg_tower_connection is an AbgCastleConnection or None (an empty index 18 reads as None), abg_connections a tuple of AbgTowerConnection or None. AllianceCrest.layout_id is an AllianceCrestLayout (or the int sent) or None instead of 0. GetHighscoreResponse.raw_list is renamed rows and holds HighscorePlayerRow, HighscoreAllianceRow, HighscoreIslandRow or HighscoreTournamentRow. RankingEntry is removed, from empire_core, empire_core.ranking and empire_core.protocol.models, with GetHighscoreResponse.entries and GetRankingListResponse.entries: read rows, and GetRankingListResponse.scores (LeaderboardScore). RankingService.get_highscore returns list[HighscoreRow]; get_ranking_list, get_own_ranking_page and get_ranking_window return list[LeaderboardScore] (player_name, not name). EventScore.from_player_row and from_alliance_row are replaced by from_highscore_row, which takes a typed row.

  • messages: MessageInfo.message_type_enum and the battle log's message_type_enum are removed; message_type is now the MessageType member (a plain int for an unknown type, kept and warned once).

  • messages,commanders,player: AbilityWaveValue.wave_id is an int (0 when missing), value a number (0 when missing) and flank_name a BattleLogFlank (or the str sent) or None. BattleLogShortResponse.advisor_type is AttackAdvisorType (NONE when absent), advisor_movement_count/number are ints, auto_skip_type is AutoSkipCooldownType or None, found_equipment is Equipment or None, found_gem is a Gem or None, supporters_wounded is a tuple of SupporterWounded (read row.player_id/row.wounded_units). MovementUnitInfo.advisor_type is AttackAdvisorType. ReadMessageResponse.extra is renamed bookmark and is a Bookmark or None. LeaderBase.alien_equipment and temporary_equipment are AlienEquipment or None (read .hero_bonuses/.bonuses); LeaderBase.active_alien_equipment is new. Equipment.alien_string is a str or None (a number arrives as its text). BoosterInfoResponse.premium_type is PremiumAccountType or None (None replaces -1) and premium_account_type() returns None, not

  • models: CreateMarketMovementRequest.goods is a dict[Resource, int] (pass {Resource.WOOD: 100} instead of [["W", 100]]). MovementMarket.goods, MovementWrapper.travel_goods, Movement.goods, BattleParticipant.loot and SpyReportResponse.resources are tuples of Collectable (read c.kind, c.item, c.amount instead of [key, amount] rows); they default to (). The MovementGoods alias is removed. BattleLogShortResponse.auto_skip_costs is a tuple of currency Collectables; found_minute_skip is a Collectable or None.

  • models: OwnerFaction.title_id is a Title (or the int sent) or None, not 0, when TID is not a number. MovementOwner.title_prefix_id and title_suffix_id are Title or None (None for -1 or a value that is not an int). MovementRecord.horse_booster_id and Movement.horse_booster_id are renamed horse_booster and are a Horse (or the int sent) or None, not -1. Equipment.gem_id is a Gem (or the int sent) or None, not -1; has_gem reads it. GemStack.gem_id is a Gem (or the int sent). ActivatingSceatSkill.skill_id is a SceatSkill (or the int sent) or None, not 0, when the entry has no ID.

  • models: These fields hold tuples of enum members (an int each) instead of lists of ints: AttackInfoResponse.defender_legend_skill_ids, BattleLogMiddleResponse.attacker_legend_skill_ids and defender_legend_skill_ids, SpyReportResponse.legend_skill_ids, SkillList.legend_skill_ids and sceat_skill_ids, General.skill_ids (and GetGeneralsResponse.skill_ids() returns a tuple), LeaderBase.alien_gem_ids and general_skill_ids. BattleLogMiddleResponse.attacker_triggered_gems and defender_triggered_gems are tuples of Gem members, () instead of None when absent. LeaderBase.general_ability_ids is renamed general_selected_abilities and holds SelectedAbility entries instead of raw pairs. SelectedAbility.ability_id is None for an empty slot instead of -1, and a SelectedAbility dumps as its [slot_id, ability_id] pair; it now lives in empire_core.commanders.models.roster (still exported from empire_core.commanders). SetGeneralAbilitiesRequest.abilities takes SelectedAbility entries instead of [slot_id, ability_id] lists. BattleLogAbility.ability_id is a GeneralAbility member or None. IslandTitle.title_id is a Title member, None instead of -1 for none (held_title_id still gives -1); AllianceCityTitle.title_id is a Title member, None for an id that is not an int. CrestLayout.layout_id is an AllianceCrestLayout member, None for one that is not an int. CastleBuildings.construction_items is a tuple of BuildingConstructionItems instead of the raw CI value. bought_research_ids, finished_achievement_ids, upcoming_event_ids, raid_boss_ids, the QuestBook lists, PermanentCastle.horse_ids (and GameState.get_castle_horse_ids) and AllianceCrest.color_ids hold Research, Achievement, Event, RaidBoss, MainQuest, Horse and AllianceCrestColor members.

  • movements: MovementArea.row is removed; read MovementArea.item, a MapAreaItem or None (item.raw_data holds the row as sent). MovementArea.model_validate takes the bare row or {"row": [...], "kingdom": KID}. MovementWrapper.auto_skip_cooldown_type and Movement.auto_skip_cooldown_type are AutoSkipCooldownType (or the int sent); Movement.advisor_type is AttackAdvisorType (or the int sent).

  • texts: Text() and cached_text() now format number arguments for the language (1,234.5; 250k) and read a string argument that is a text id as its text; for the old plain filling use fill(get_texts(lang)[key.lower()], *args).

v0.49.0 (2026-10-07)

Bug Fixes

  • combat: Add the global effect booster to boosted effects' unit attack bonus (0f71214)

  • pool: Keep a kept client that is logging back in (dc4fade)

  • state: Fire on_movement_arrived at the arrival time (bf767d0)

Chores

  • logging: Log a session dropped mid-scan once, as a warning (321f369)

Documentation

  • A general landing page and README, with an alpha warning (1beb28b)

Features

  • army: Name the spy-army sections with SpyArmySection (6cd9762)

  • client: Configurable keep_session timing, a restoring state, and first-login retries (3cb11c4)

  • map: Scan one kingdom with several clients (00be8c7)

  • rewards: Collect the patch note rewards and read the pending rewards (6bd73bd)

  • state: Reannounce a movement the post-reconnect movement list has not listed yet (4410e36)

Breaking Changes

  • army: SECTION_NAMES is removed from empire_core.army.spy_army; iterate SpyArmySection (from empire_core.army) instead. WALL_SECTIONS is removed; use SpyArmySection.is_wall. SpyArmy.sections() yields SpyArmySection members as its first element. A member still compares equal to the old name, but str() and, on Python 3.12+, f-strings show the member (SpyArmySection.LEFT), so format a section name with .value.

  • client: Empire_core.client.session.RELOGIN_FIRST_DELAY and RELOGIN_MAX_DELAY are removed; set EmpireConfig.relogin_first_delay and EmpireConfig.relogin_max_delay instead. Assigning a value an EmpireConfig field does not take now raises pydantic's ValidationError where it was stored as given; assign values of the field's type, and raise relogin_max_delay before relogin_first_delay when raising both.

  • combat: AttackService.fill_waves and fill_attack replace global_effect_ids (ids or [id, seconds_left, strength] rows) with global_effects, an iterable of GlobalEffectTimer, read from the Event.GLOBAL_EFFECT event in state when not given. Drop the argument to use the state's running effects, or pass GlobalEffectEvent.effects (or your own GlobalEffectTimer objects) instead of the ids or rows.

  • pool: A keep_alive pool no longer closes and replaces a kept client that is logging back in (is_restoring_session); its account is skipped until the restore ends, so a lease by that username returns None (leased() raises PoolExhaustedError) instead of logging in a fresh client. release() (and the end of a leased() block) now keeps such a client instead of closing it. Retry later, or close it yourself with pool.get_client(username).close() to force a fresh login on the next lease.

v0.48.0 (2026-10-06)

Documentation

  • combat: Point global effect strengths at the GLOBAL_EFFECT event (#384, b3d54c5)

Features

  • alliance: Add, change and delete map bookmarks (#383, 9f47a0c)

  • events: Read your own rank and points in an event (#378, 6a2169f)

  • rewards: Read and collect the free daily rewards (#382, 4e05cc7)

  • state: Read quests, daily quests and more event types into state (#379, dc39fe2)

  • state: Read the daily reset, attack counter, officers' school, gifts and wishing well (#380, 58ae363)

  • state: Read the gems, loot boxes, inventory space, kingdoms, mercenaries and tax (#381, 754a63b)

Breaking Changes

  • state: The events listed above are now their own SpecialEvent subclasses instead of a plain SpecialEvent, so code checking type(event) is SpecialEvent sees them differently; use isinstance. For those whose client class does not read KL (all of them but the fortune teller and the score events 36 and 129), kingdoms_league_mode is now always False, as the client keeps it. LuckyWheelEvent subclasses PointEvent, so isinstance(event, PointEvent) is now true for the lucky wheels (15, 89) too; check the event id to tell the nobility contest. The alliance tournament (36) and mobilisation (129) now match ScoredEvent. An entry their client class fails to read is no longer stored: 36 without A, 129 without A or SP and 96 without CQS leave get_event() returning None (or the event as it was) and fire no on_event_added. A pep for 36, which changed nothing before, now updates the alliance score in parts["A"].

v0.47.0 (2026-10-05)

Bug Fixes

  • network: Keep a late reply from the next request for its command (#360, 9866cc0)

  • services: Report failure by one rule, and match server errors by name (#363, 8225114)

Features

  • client: Log a dropped session in again with keep_session (#370, 6e59912)

  • state: Occupation events after a capture attack (#367, 54134e2)

  • state: Re-announce tracked attacks, wall-clock protection end, listen() by name (#368, 8385303)

Performance Improvements

  • map: Hand each scanned chunk to on_chunk instead of keeping the kingdom (#369, fe487ea)

Refactoring

  • Declare each callback once as an Event, and remove it with .remove() (#374, c98f2fe)

  • Login() returns None, and the last field comments become descriptions (#364, 10ebd7a)

  • Make the root and the first-level packages the public import paths (#365, 959d761)

  • attack: Split service.py into sending, target reading and wave filling (#362, 385e465)

  • client: Move the login and session upkeep out of EmpireClient (#372, 2163f0f)

  • gamedata: One items download and cache for game data and troop counts (#361, faee81d)

  • state: One event registry for every callback (#371, 778e7b8)

  • state: Split movement storage from announcing (#373, a2711e3)

Breaking Changes

  • EmpireClient.login() returns None instead of True. Drop any check on its return value; catch EmpireError (or a subclass) for failures.

  • Every remove_callback(cb) is gone; call on.remove(cb) instead. Released in v0.46.0: on client.state remove_incoming_attack_callback, remove_incoming_attack_updated_callback, remove_incoming_attack_withdrawn_callback, remove_movement_arrived_callback, remove_movement_recalled_callback, remove_movement_removed_callback, remove_building_finished_callback, remove_building_xp_callback, remove_buildings_changed_callback, remove_event_added_callback, remove_event_removed_callback, remove_events_updated_callback; on client.alliance remove_chat_message_callback and remove_help_update_callback; on client.skills remove_skill_list_callback; on client.messages remove_new_messages_callback; on the client remove_disconnect_callback. The unreleased remove_occupation_{started,updated,ended}callback and remove_session_callback go too.

  • Module paths below the first level are no longer public, among them empire_core.protocol.models, empire_core.protocol.errors, empire_core.protocol.base, empire_core.protocol.auth, empire_core.gamedata.ids, empire_core..models.*, empire_core..service, empire_core.map.scanner and the whole of empire_core.utils. Import each name from the root or its first-level package instead: a model from its area (empire_core.castle.GetCastlesRequest), a service from its area (empire_core.map.MapService), the scanner and its results from empire_core.map, an id enum from empire_core.gamedata (empire_core.gamedata.Event), the protocol bases, codecs, login models and GGEError from empire_core.protocol (GGEError, Packet and the codecs also from the root), get_event_titles from empire_core.events, and the troop and CDN helpers from empire_core.gamedata. These have no public path any more and may change or go without notice: empire_core.utils.cancel.sleep_unless_cancelled; empire_core.network (Connection, ResponseWaiter, FrameBuffer); empire_core.client.stream.callback_sources and CallbackSource; the protocol helpers build_command, build_version_check, json_text, smartfox_text, is_smartfox_valid, enum_or_none, list_or_empty, object_or_none, read_or_none, readable_list and the empire_core.protocol.js readers; and the internal helpers building_or_none, building_rows, unlocked_slots, rows_by_id, EffectSpecRow, owner_positions, island_title_chain, legend_spy_bonus, research_spy_bonus, title_spy_percent, max_damaged_buildings, max_sabotage_damage, risk_target_flags and row_risk_flags.

  • gamedata: Empire_core.utils.troops and empire_core.utils.events are gone. Import get_troop_ids, troop_data_available, count_troops, get_items_version and fetch_items_data from empire_core.gamedata (the first two also stay on empire_core), and get_event_titles from empire_core.events. GameData.load returns the same shared instance for every call while the items version holds (treat it as read-only). When the CDN fails it returns the data already loaded; with nothing loaded it raises NetworkError, and within five minutes of a failed fetch it does so without a request unless refresh=True.

  • network: After a request times out, the next request for a command without a reply check first waits for the owed reply, up to the timed-out request's timeout (or send_and_drop_reply's window), and then has its own whole timeout, so the call can take that much longer than its timeout. Connection gains send_and_drop_reply(data, cmd_id, window), which get_movements(wait=False) and the mvf handler call: a stand-in Connection must provide it. get_movements(wait=False) can now raise EmpireTimeoutError when a gam request holds the command for all of its timeout. An arc reply without CID (or with another castle's CID or KID) is not taken by a RenameCastleRequest (castle.rename), whose request then raises EmpireTimeoutError instead of PacketError.

  • services: Client.spy.forward_report(message_id, []) raises ValueError instead of returning False; pass at least one player id. client.alliance.get_local_members() and get_local_online_members() raise NotInAllianceError outside an alliance instead of returning [], and get_member(player_id, no_cache=True) raises it when no alliance is cached and you are in none; check client.alliance.local_alliance_id first or catch the error. client.alliance.local_alliance_id is None when the player data's AID is negative, where it returned the negative id. client.alliance.get_chronicle(), get_applications(), answer_application(), invite(), kick_member(), set_rank(), leave(), change_diplomacy(), refuse_diplomacy(), set_auto_war(), send_newsletter() and donate() raise NotInAllianceError outside an alliance without sending, and for the server's ALLI_NOT_FOUND, where they sent and raised CommandError or returned False; get_subscriber_count() raises it for ALLI_NOT_FOUND. client.alliance.kick_member() and refuse_diplomacy() return AllianceInfo, not AllianceInfo | None, and with set_rank() raise PacketError for a reply without the alliance; set_rank() returns None only for NO_CHANGE. client.castle.get_details() raises UnknownCastleError instead of returning None for an id the reply does not list, and client.castle.get_horses() raises it for a castle not in your castle list. client.events.get_league_id() returns int and raises EventNotRunningError instead of returning None for an event that is not running. client.alliance.search_alliances() raises PacketError when the hgh payload is not an object, where it returned []. client.attack.fill_attack() raises timeouts, dropped connections and unreadable replies of its target reads (the map scan, the pre-calculation, gie and skl), where it filled without them, and raises ValueError for an area type with no pre-calculation modelled; read FilledAttack.unread, a dict of TargetRead to CommandError, for the reads the server refused, which it logs at warning level (info for the pre-calculation's INVALID_AREA) instead of debug. ClientVersionError.status is a VersionCheckStatus (still equal to 1 or 2).

  • state: Movement.is_siege and MovementType.is_siege are renamed to is_occupation; use movement.is_occupation. An occupation no longer counts as incoming: Movement.is_incoming is False for one and get_incoming_movements() leaves it out; use get_occupations() or on_occupation_started for occupations of your areas.

v0.46.0 (2026-10-05)

Features

  • client: Stream every callback to an asyncio loop with client.listen (#350, 5cd99e0)

  • combat: Resolve commander bonuses from a Movement, with set bonuses (#346, 733502d)

  • commanders: Expose free premium commanders and never charge rubies silently (#347, dbbcd56)

  • map: Cancellable scans (#352, 5559c0d)

  • player: Fix peace and beginner protection end times at parse time (#348, 3dab27c)

  • pool: Keep leased clients logged in between leases (#349, dd60b81)

  • spy: Make a spy mission cancellable and its report awaitable separately (#351, 64aa311)

  • state: Add an incoming_attack_updated callback (#345, 6fa5348)

  • state: Report when an announced attack is withdrawn before it arrives (#344, 6edd3a7)

Performance Improvements

  • map: Build only the map rows a scan keeps, and owner positions without pydantic (#354, 1718ebd)

Testing

  • state: Check a booster's end on either side of the boundary (#355, 23d66b2)

Breaking Changes

  • combat: Commander_bonuses, attack_dialog_bonuses, the wave fills built on them and defender_flank_effects (through the castellan) now include equipment set bonuses, so a commander or castellan wearing set items resolves higher than before. Nothing to change in calling code; expect the larger totals. A default commander (commander_id below 0) now resolves to its lords row's effects, then E and AE, and any equipment it is sent with no longer counts. An area_type or space_id below 0 passed to the effect resolver (and the functions built on it, such as attacker_flank_effects) now keeps every effect, where it used to drop the effects scoped to an area or space.

  • Movement.commander_equipment and Movement.commander_effects are removed. Read movement.commander.equipment and movement.commander.area_effects instead; movement.commander is None when the movement carries no commander.

  • commanders: Send_support, send_troops and send_attack led by the premium commander (use_premium_commander=True or commander_id=-14) now raise PremiumCommanderCostError, sending nothing, unless a free premium commander is left or a premium account runs; pass spend_rubies=True to send anyway and pay rubies as before. While VIP time runs the check needs client.load_game_data() and raises GameDataNotLoadedError without it.

  • player: PlayerProfileBase.bird_end_time and GetPlayerInfoResponse.bird_end_time (a UTC datetime counted from the moment of reading) are removed. Use revenge_protection_end (time.monotonic() seconds, fixed at parse) or remaining_revenge_protection_seconds(). has_bird on PlayerProfileBase (AllianceMember, PlayerOwnerInfo) and GetPlayerInfoResponse now turns False once the peace protection has run out, instead of staying True for as long as the record is kept; read revenge_protection_seconds for the value as sent.

v0.45.0 (2026-10-05)

Build System

  • Check pydantic constructors with mypy's plugin (#342, 772c1b9)

Features

  • attack: List, save and rename attack presets (#337, 91a14f0)

  • castle: Read, start and collect tax (#335, 49bd3e7)

  • commanders: Read the general data a commander entry carries (#334, ed9e70b)

  • defense: Set the keep, wall and moat defense (#336, a7bb9fb)

  • map: Find the next enemy castle and Berimond tower (#338, 99312ee)

  • messages: Read battle reports and forward them (#334, ed9e70b)

  • state: Apply castle pushes to state (#333, 63e5709)

  • state: Keep the commanders, legend skills, alliance details and chat from login (#331, db643ba)

  • state: Read the player's progress into state (#332, 476c243)

Refactoring

  • combat: Type-check cleanly without ignores (#342, 772c1b9)

  • enums: Rename SpyLogResult to LogResult (#334, ed9e70b)

  • messages: Make messages.models a package (#334, ed9e70b)

Testing

  • Scrub real names from fixtures and examples (#339, 649ba3b)

Breaking Changes

  • commanders: A commander or castellan entry's XP, OXP, IN, LU, SIDS and GASAIDS are typed fields (general_xp, general_old_xp, general_is_new, general_has_level_up, general_skill_ids, general_ability_ids) and are no longer in model_extra.

  • enums: SpyLogResult -> LogResult (same members and values), imported from empire_core.enums, empire_core.spy or empire_core.protocol.models.

  • state: CommanderRoster (gli, and every reply carrying one) now lists commanders sorted by commander_id and castellans by portrait order instead of in the server's order. ChatMessageData is frozen: assigning to its fields raises.

  • state: SelectCastleResponse reads the jaa reply's gsm, rci, csl and gab blocks as typed fields (mines, resource_carts, slum_level, area_booster); they are no longer in model_extra. MineStatusList and ResourceCartInfo are now the registered gsm and rci response models. MineStatus and ResourceCart are frozen: assigning to their fields raises.

  • state: Total_spies() and available_spies() default research_ids, legend_skill_ids, title_ids and island_title_id to None (read from state) instead of () and -1. A call without arguments after login now counts the boosts and raises GameDataNotLoadedError unless client.load_game_data() was called; pass () / -1 for the old count.

v0.44.0 (2026-10-02)

Features

  • alliance: Read the alliance chronicle (all) and subscriber count (asc) (#321, ed9dc6e)

v0.43.0 (2026-10-01)

Bug Fixes

  • state: Decide alliance attack alerts as the client does, and correct command descriptions (#316, 5f2345f)

  • state: Decide alliance attack alerts as the client does, and correct four command comments (#316, 5f2345f)

  • state: Read trigger events and every sei the client applies (#313, 6ffd29e)

  • state: Stop reading lli as login data, and correct two descriptions (#316, 5f2345f)

Documentation

  • design: Bring the design notes in line with client a4a25ae6 (#269, 0dd3fe9)

Features

  • events: Get an event's scores by its name (#276, 44f5904)

  • events: Read client.events from state and add refresh() (#315, e36fc60)

  • ranking: Refuse leaderboard replies for another list (#266, 9d04aa6)

  • spy: Read the spy count into state and count available spies as the client does (#267, 8513c20)

  • state: Keep events as typed models in GameState (#314, b6c94cc)

Refactoring

  • enums: Name NPC owner ids in one NPCOwner enum (#316, 5f2345f)

  • gamedata: Load each id enum module on first use (#276, 44f5904)

Testing

  • protocol: Read SmartFox system messages and sent server constants from the client (#268, 13fc335)

Breaking Changes

  • enums: Empire_core.movements.tracked.DAIMYO_TOWNSHIP_PLAYER_ID is removed; use NPCOwner.DAIMYO_TOWNSHIP.

  • events: A sei packet no longer ends the events it does not name, and an event now ends when its RS runs out (one never given RS is not running): client.events.get_active_event_ids() and GameState.active_event_ids (now a read-only copy) follow that, and a see push ends an event. get_league_id(event_id, part=None) gains an optional part (additive).

  • events: GameEvent is a pydantic model from empire_core.events (empire_core.GameEvent still names it); empire_core.utils.events.GameEvent is gone. id -> event_id; internal_name (the CDN eventType) and description (its developer comment) are gone, use event (an Event member, or None) and display_name; details is new and dumps with its model's own fields. get_active_events() now returns every running event, the trigger events and ids the CDN does not know included (it skipped ids missing from the CDN items), and no longer raises NetworkError when the CDN is down. empire_core.utils.events.get_active_events() is gone (use client.events.get_active_events(), or get_event_titles()).

  • ranking: Send()/request() for llsp and llsw no longer return a successful reply for another list in the league asked for, and slse no longer returns one for another list; they wait for the matching reply and raise EmpireTimeoutError if none comes.

  • spy: SpyService.spies_in_use() counts only your movements of MovementType.SPY, not every movement of yours that carries an S block.

  • state: Active_event_ids (and client.events.get_active_event_ids()) now include the running trigger events (601, 610, 612); a sce amount the client reads as 0 is stored as 0 rather than skipped, and "12.7" is stored as 12.

  • state: An lli payload is no longer applied to state or stamped: it no longer updates the local player, and GameState.get_last_packet_time("lli") always returns None.

  • state: GameState.active_event_ids, event_end_times, event_league_ids, event_part_league_ids and event_unlocked, and get_event_league_id() and is_event_unlocked(), are gone: use get_events(), get_event(event) and the models' fields (league_id, parts[...].league_id, BerimondEvent.unlocked, end_time). client.events.get_league_id() returns 1, not None, for a running event or part without a league.

  • state: On_incoming_attack now also fires for attacks on alliance members by alien attacks, the alliance nomad camp and NPCs that are not dungeon owners (outpost, capital and metropolis owners, the plague monk, unknown NPC ids), fires for an attack whose owner record arrives after the movement, and no longer fires for an alien attack on the daimyo township or a movement without an owner id. Movement.is_incoming is False for an alien attack on the daimyo township.

v0.42.1 (2026-10-01)

Build System

  • List the supported Python versions and project classifiers (#262, 2a8424e)

Documentation

  • A calmer README and site, with a logo (#262, 2a8424e)

  • A calmer theme, a new logo and social preview, and a split home page (#262, 2a8424e)

  • api: Render the reference's docstrings as Markdown and give each module a real summary (#262, 2a8424e)

  • readme: A calmer showcase with the logo, a recorded run and a service table (#262, 2a8424e)

Refactoring

  • castle: Annotate model fields with ClientInt instead of duplicate _int validators (#282) (#317, d8fee77)

v0.42.0 (2026-09-30)

Bug Fixes

  • Mailbox and help list from the login data, join_area on the castle service, event leaderboard leagues (#242, 4cb3c19)

  • alliance: Fill the help list from the login data and stop sending ahl (#242, 4cb3c19)

  • alliance: Rank 0 is the alliance leader, not rank 8 (#247, a2848ec)

  • alliance: Read bookmarks flat, and read your own bookmarks too (#247, a2848ec)

  • alliance,player,messages: Follow up the alliance review (#247, a2848ec)

  • attack: Take every camp's wall, gate and moat protection from its row, and its level where the row gives it (#246, b9baf27)

  • auth: Keep login secrets out of reprs and logs, and log login refusals at debug (#244, b581bec)

  • auth: Model the auth requests and replies on the client's VOs (#244, b581bec)

  • auth: Send lpp as the client does, with the email under MAIL (#244, b581bec)

  • castle: Move join_area to the castle service and return the joined area's state (#242, 4cb3c19)

  • castle: Name PlayerCastle's occupier as the client and the map rows do (#246, b9baf27)

  • castle: Read and send castle, building and resource commands as the client does (#245, 81404fc)

  • castle: Read reply blocks as leniently as the client (#245, 81404fc)

  • castle: Send dcl's CD as the client does (#245, 81404fc)

  • client: Ask for movements only after an mvf push, as the client does (#243, bd18972)

  • client: Hand only successful replies to on_response handlers (#243, bd18972)

  • client,state: Re-list movements after a login, announce each attack once, ignore late drop reports (#243, bd18972)

  • gamedata: Read horse rows as the travel booster does (#259, bd6fb65)

  • login: Keep an slt login token whatever JSON type it arrives as (#244, b581bec)

  • login: Name a refused login's GGEError and keep its code on LoginError (#252, fdd7577)

  • login: Name a refused login's GGEError, and frame requests with the session's zone and room in one place (#252, fdd7577)

  • login: Require joinOK into the lobby before the version check (#244, b581bec)

  • login: Run the client's handshake and build every frame as the client does (#244, b581bec)

  • map: Read a uap block's peace mode and faction protection too (#246, b9baf27)

  • map: Read map rows and owner records as the client does (#246, b9baf27)

  • map: Read map rows per area type, and add fnm, uap and kingdom on every item (#246, b9baf27)

  • map,player: Read dummy owner records and wsp's gaa block as the client does (#246, b9baf27)

  • messages: Empty the mailbox only when the current session drops (#247, a2848ec)

  • messages: Fill the mailbox from the login data's sne section (#242, 4cb3c19)

  • messages: Tell spy logs by MessageType and read their header decoded (#247, a2848ec)

  • network: End the receive loop cleanly whatever stops it (#255, a1c67f9)

  • network: Keep the session when splitting a message into packets fails (#255, a1c67f9)

  • network: Run one request per command at a time and check gdi replies (#243, bd18972)

  • network: Send nothing once the lock wait used up the timeout, refuse the lock on the receive thread (#243, bd18972)

  • network: Split the message stream into packets as the client does (#243, bd18972)

  • network,state: Split frames, serialise requests and reset state on a drop as the client does (#243, bd18972)

  • player: Encode a player search's name as chat text, as the client does (#247, a2848ec)

  • player: Read the owner record as WorldMapOwnerInfoVO does (#247, a2848ec)

  • player: Say which players a bulk lookup found, which failed and which timed out (#247, a2848ec)

  • pool: Free the account when a login is interrupted (#243, bd18972)

  • pool: Make AccountPool thread-safe and take its registry explicitly (#243, bd18972)

  • protocol: Drop Packet.iter_from_bytes, which split frames unlike the client (#244, b581bec)

  • protocol: Drop the ani, gfc and sem commands the client no longer has (#244, b581bec)

  • protocol: Rename the login cooldown code as the client does and add codes 454 to 463 (#244, b581bec)

  • protocol: Send commands in the joined room and escape them as the client does (#244, b581bec)

  • protocol: Write command JSON and params exactly as the client does (#244, b581bec)

  • protocol: Write outbound frames without a trailing null byte (#244, b581bec)

  • ranking: Require the league for event leaderboard pages and expose each event's league (#242, 4cb3c19)

  • spy: Cost NPC dungeons without the player floor and match the log to the mission (#238, 3d5b5d8)

  • spy: Model spy reports and the spy screen fully, and add sabotage, auto-spy and reports by id (#238, 3d5b5d8)

  • spy: Name the horse parameter horses_type, as the other senders do (#238, 3d5b5d8)

  • spy: Read ssi's uap block as map's KingdomProtection (#246, b9baf27)

  • spy,messages: Model spy reports and the spy screen fully, and type spy outcomes (#238, 3d5b5d8)

  • state: Forget the lost session's data when the connection drops (#243, bd18972)

  • state: Run state callbacks one at a time, in packet order (#243, bd18972)

Build System

  • ci: Test on Python 3.13 and 3.14 and report coverage (#243, bd18972)

Code Style

  • Format the new observability and decoding code (#255, a1c67f9)

Documentation

  • A documentation site with MkDocs Material, and a showcase README (#260, 575f2fa)

  • Build a documentation site with MkDocs Material (#260, 575f2fa)

  • Cover the castle-list kingdom lookup, typed market goods and castle horses (#260, 575f2fa)

  • Drop a private project's name from the changelog and pin MkDocs below 2 (#260, 575f2fa)

  • Rewrite protocol.md from the client and drop docs that describe no real API (#248, d87e6c7)

  • login: Say the slt login token arrives just after login() returns (#244, b581bec)

  • models: Name the client code behind every command model (#240, 24768df)

  • protocol: Say outbound frames carry no null byte, and what vck and pin send (#248, d87e6c7)

  • readme: Turn the README into a short showcase that links to the docs (#260, 575f2fa)

  • state: Say which thread state callbacks run on and what a drop does to state (#243, bd18972)

Features

  • Send troops between your own areas, and refuse attacks below the client's minimum army (#251, 3a97e6b)

  • alliance: Diplomacy, auto war, the newsletter and donations (#247, a2848ec)

  • alliance: Help list, member management, diplomacy, donations and mail (#247, a2848ec)

  • alliance: Kick, rerank, invite, applications and leave (#247, a2848ec)

  • alliance: Read peace offers, crest layouts, crests and member stats (#247, a2848ec)

  • alliance: Read the alliance help list and send help as the client does (#247, a2848ec)

  • attack: Refuse attacks below the client's minimum army (#251, 3a97e6b)

  • castle: Read each castle's usable horses from gpc (#259, bd6fb65)

  • castle: Send troops between your own areas with cat as the client does (#251, 3a97e6b)

  • castle: Type the goods a market send carries (#258, 2918734)

  • client: Add on_disconnect and remove_disconnect_callback (#243, bd18972)

  • config: Build the network.xml URL from the client's host parts (#244, b581bec)

  • config: Read the server list from network.xml (#244, b581bec)

  • login: Run the client's handshake, with its version check, timings and token (#244, b581bec)

  • map: Find map objects with fnm as the client asks, read uap and join areas with jaa (#246, b9baf27)

  • messages: Name the mailbox message types and decode headers as the client does (#247, a2848ec)

  • messages: Read, mark, archive, delete and send mail (#247, a2848ec)

  • movements: Keep a spy movement's S block on tracked movements (#238, 3d5b5d8)

  • movements: Recall a movement with mcm as the client does (#250, 4569440)

  • network: Refuse to wait for a reply on the receive thread (#243, bd18972)

  • network: Tell waiting calls what ended the connection (#261, 3efb59b)

  • protocol: Take only the reply that names the request for more commands (#255, a1c67f9)

  • ranking: Add llsw and slse, and check the library's commands against the client (#248, d87e6c7)

  • ranking: Add llsw and slse, and send the alliance event keys (#248, d87e6c7)

  • spy: Ask ssi once unless told to wait for spies (#255, a1c67f9)

  • state: Apply the castle list a faction join reply carries (#257, 00af2ba)

  • state,network: Expose the callback queue depth and the degraded-frame counts (#255, a1c67f9)

Performance Improvements

  • Handle high-load traffic, and drop waits the client doesn't have (#255, a1c67f9)

  • attack: Stop rejoining the attacking castle after fill_attack scans (#255, a1c67f9)

  • map: Keep owner castle positions as named tuples (#255, a1c67f9)

  • map: Scan without pacing and back off only when a chunk fails (#255, a1c67f9)

  • network: Decode messages once, as the client does, without websocket-client's UTF-8 check (#255, a1c67f9)

  • network: Hand whole packets straight through when nothing is pending (#255, a1c67f9)

  • network: Keep split packets as chunks instead of rescanning the buffer (#243, bd18972)

  • network: Read system messages from their opening tag, not the whole buffer (#255, a1c67f9)

  • player: Fetch bulk player details without a delay between requests (#255, a1c67f9)

  • state: Scan movements only once one is due (#255, a1c67f9)

  • state: Validate a movement wrapper once and build the Movement in one validation (#255, a1c67f9)

  • state: Validate a movement's record once, not again inside its wrapper (#255, a1c67f9)

Refactoring

  • Name the horse booster field horse_booster_id everywhere (#256, 8b53726)

  • Remove the unused async storage module (#243, bd18972)

  • Spy enums in the enums package, one name for the horse booster (#256, 8b53726)

  • castle: Take the kingdom from the castle id (#257, 00af2ba)

  • castle: Take the kingdom from your castle list, keyed per kingdom (#257, 00af2ba)

  • client: Frame requests with the session's zone and room in one place (#252, fdd7577)

  • services: Build the services directly instead of through a registry (#239, 4e584c8)

  • spy: Define SpyStep and SpyOutcome in the enums package (#256, 8b53726)

Testing

  • Replace live-looking ids and coordinates across the tests with one made-up set (#241, cf6f711)

  • Replace live-looking ids in the shared test helpers (#238, 3d5b5d8)

  • Type the new fakes and requests for mypy (#255, a1c67f9)

  • castle: Make the gcl and dcl fixtures' player and castle ids and coordinates up (#245, 81404fc)

  • client: Check that a command after the login carries the joined room (#244, b581bec)

  • login: Check the code on every typed login refusal, and cite the client for frame() (#256, 8b53726)

  • movements: Shape the recalled movement like a live mcm reply (#250, 4569440)

  • protocol: Check the plain command ids the code uses, too (#248, d87e6c7)

  • protocol: Fail when a command the library uses is not in the game client (#248, d87e6c7)

  • protocol: Read build_command frames in the command check, and drop the aha and ahc reply exceptions (#248, d87e6c7)

Breaking Changes

  • Empire_core.storage (GameDatabase and its write queue) and the empire-core[storage] extra are removed.

  • Horses_type is renamed horse_booster_id on CreateAttackRequest, SendSupportRequest, SendTroopsRequest, CreateMarketMovementRequest and SendSpyRequest and on the send_attack, send_support, send_troops, send_resources, send_spy_mission, execute_instant_spy and send_sabotage parameters; SendSpyRequest.pay_to_travel is renamed feathers.

  • alliance: AllianceBookmark -> Bookmark, GetAllianceBookmarksRequest -> GetBookmarksRequest, GetAllianceBookmarksResponse -> GetBookmarksResponse (bookmarks -> alliance_bookmarks, plus own_bookmarks). Bookmark.owner is a PlayerProfileBase instead of a MapObject, and None for a record without an OID. AllianceService.get_bookmarks() returns the GetBookmarksResponse instead of the ABL list.

  • alliance: AllianceInfo.members is sorted by alliance_rank; a member entry without an OID is skipped instead of failing validation or reading as player 0; application_count defaults to 12 instead of 0; GetAllianceInfoResponse.alliance is None for an A block without an AID.

  • alliance: AllianceService.get_help_requests() and AllianceHelpListRequest are removed; read client.alliance.help_requests, which the login data now fills.

  • alliance: HelpType.HEAL -> HelpType.HEAL_UNIT and HelpType.RECRUIT -> HelpType.RECRUITMENT_LIST, and RECRUITMENT, BUILD and LOOP_RECRUIT are new. HelpMemberRequest is {LID, KID} (list_id, kingdom_id) and its heal/repair/ recruit constructors are replaced by for_request(entry). AskHelpRequest is {ID, T} (target_id, type_id) with build/repair/recruit/heal constructors taking the ids the client sends. HelpAllRequest sends KID 15 and has no help_type. HelpAllResponse, HelpMemberResponse and HelpRequestNotification are removed. AllianceService.help_all() returns None instead of HelpAllResponse; help_member_heal/help_member_repair/help_member_recruit -> help_member(request); request_heal_help(castle_id) -> request_heal_help(hospital_entry_id, hospital_list_id); request_repair_help(castle_id, building_id) -> request_repair_help(building_id); request_recruit_help(castle_id) -> request_recruit_help(recruit_id, help_type); the request_* methods return bool.

  • alliance: PlayerProfileBase.is_leader (and so AllianceMember and PlayerOwnerInfo) is True for alliance_rank 0 instead of 8, and is_leader and is_officer are False for a player outside an alliance.

  • alliance,player,messages: PlayerProfileBase (AllianceMember, PlayerOwnerInfo) loses glory_points (CF), highest_glory_points (HF) and title_index (TI); they stay in model_extra. alliance_rank is int | None, None for a missing or unreadable AR, where it was 0. AllianceInfo.members puts members without a rank last. MessageInfo.seconds_since_sent may be None. Also missing from the bookmark commit's footer: Bookmark.name is str | None defaulting to None, where AllianceBookmark.name was str defaulting to "".

  • attack: After a fill_attack that scanned the target's tile the session is left on the map; castle-scoped reads other than client.army methods need client.castle.select() first.

  • attack: Fill_attack raises AttackBelowMinimumError when the waves carry fewer units than the target's minimum, where it used to return them; send_attack(capacity=...) raises it before sending where it used to send and return False.

  • auth: LoginRequest fields are the client's (NOM, PW, LT, LANG, CONM, RTM, ...): username/password/language/app_id aliases NM/PW/ L/AID become NOM/PW/LANG/AID with the other C2SLoginVO keys added. PlayerData is removed, as are LoginResponse.player and session_id. RegisterRequest sends PN/PW/... instead of NM/PW/EM/L and has no email; RegisterResponse.error_message is replaced by player_id and suggested_names. CheckUsernameAvailableRequest and CheckUsernameExistsRequest send PN and NOM instead of NM; CheckUsernameAvailableResponse.available and CheckUsernameExistsResponse.exists are removed.

  • auth: PasswordRecoveryRequest has one required field, email, sent as MAIL; the EM and NM fields (email, username) are removed.

  • castle: Building requests take object_id (OID) instead of castle_id/building_id (CID/BID): BuildRequest.building_type (BT) -> wod_id (WID) plus rotation, pay_with_rubies, private_offer_id and district_object_id; TimeSkipBuildingRequest.item_id (IID) -> minute_skip (MST); UpgradeWallRequest.wall_type (WT) -> object_id; BuyExtensionRequest.extension_type (ET) -> x, y, rotation, expansion_type (CT); CollectExtensionGiftRequest.extension_id (EID) -> object_id; RepairAllRequest has no fields. Every building reply's old fields (building_id, completion_time, new_level, resources_gained, rubies_spent, new_completion_time, buildings_repaired) are replaced by the blocks the client reads. GetResourcesRequest sends AID and KID; GetResourcesResponse is the flat grc block (castle_id, kingdom_id and the 11 resources) instead of resources/storage_capacity; client.castle.get_resources(castle_id, kingdom_id) returns it instead of ResourceAmount | None. GetProductionRequest has no castle_id; client.castle.get_production() takes no castle id and returns CastleProductionArea instead of a (production, consumption) pair. ProductionRates and protocol.base.ResourceAmount are removed. GetResourcesRequest/Response and GetProductionRequest/Response moved from castle.models.actions to castle.models.resources. CastleInfo fields have no AI[n] aliases; CastleInfo.abandon_outpost_seconds, cancel_abandon_seconds and no_abandon_seconds default to 0 instead of -1.

  • castle: CastleService.send_resources annotates goods as dict[Resource, int] (a valid wire key such as "W" still converts); unknown keys, zero, negative or non-int amounts, an empty dict and goods from more than one tab now raise UnsendableGoodsError instead of being sent.

  • castle: Client.map.join_area(x, y, kingdom) -> bool is now client.castle.join_area(x, y, kingdom_id) -> SelectCastleResponse, which raises CommandError on a refusal instead of returning False. JoinAreaRequest moves from empire_core.map (and empire_core.map.models) to empire_core.castle (and empire_core.castle.models), and its field kingdom is now kingdom_id; empire_core.protocol.models still exports it.

  • castle: PlayerCastle.capturer_id -> occupier_id, PlayerCastle.is_being_captured -> is_occupied.

  • castle: Signatures changed (old -> new):

  • castle: StorageCapacity fields and Resources.wood_cap, stone_cap and food_cap are int | float instead of int, and ConstructionList.slot_count is int | float. Also missing from the footer of 1610ee9: TimeSkipBuildingRequest.item_id (IID, an int) became minute_skip (MST, a str).

  • client: On_response handlers (and client._register_handler callbacks) no longer receive replies with a non-zero error code.

  • client,state: Connection.on_disconnect is now called with the generation of the session that dropped (Callable[[int], None]); listeners added with add_disconnect_listener are still called with no arguments. The client sends gam on mvf only; the server pushes gam after login.

  • gamedata: HorseStats.unit_boost, market_boost and spy_boost are int, not float.

  • login: A refused login's LoginError message is now "Auth failed: ()" instead of "Auth failed with code ", and the token refusal message carries the code too.

  • login: Login() fails with EmpireTimeoutError ("Room join (joinOK) timed out") when joinOK does not arrive, and with LoginError when the joined room is not the lobby, instead of logging in with room

  • login: LOGIN_DEFAULTS no longer has CONM, RTM or LT; CONM and RTM are measured. The zone login sends the configured client build number. login() sends vck and fails with ClientVersionError when the server refuses the version, and EmpireTimeoutError when vck is not answered; the verChk timeout message is now "API version check (verChk) timed out". The lli name and password are encoded with encode_json_text. A banned account raises AccountBannedError and an account on another server WrongServerError (both LoginError subclasses) instead of a plain LoginError. The missing-credentials message is "Username and a password or login token are required". Generated AIDs are no longer padded to 19 digits.

  • map: FindNPCRequest (NT, L, KID) -> FindNextMapObjectRequest (area_type/T, kingdom/KID, min_level/LMIN, max_level/LMAX, owner_id/NID). FindNPCResponse (npcs) -> FindNextMapObjectResponse (x, y, area). NPCLocation is removed.

  • map: MapAreaItem.item_type is MapItemType, and from_list(row, kingdom=Kingdom.GREEN) raises ValueError for an empty row or an area type the client reads no row of. owner_id is int | None (None where the type names no owner; NPC owners stay negative, e.g. -300, instead of -1). location_id is int | None as sent (-300 stays -300). keep/wall/gate/tower/moat_level are None for a type without levels (was 0). is_relocating means a four-field castle row naming a player (was field 19 of a type-1 row); get_moving_flags keys on that player (occupier_id). Removed: MapAreaItem.player_id (use owner_id), has_owner_field (owner_id is not None), capturer_id and is_being_captured (occupier_id, is_occupied), camp_kingdom_id (kingdom), invasion_camp_field (victory_count, or camp_id for daimyo camps), type_name (item_type.name), is_moving_flag, OWNED_AREA_TYPES and FACTION_LANDMARK_TYPES. INVASION_AREA_TYPES includes NOMAD_CAMP. parse_area_rows keeps rows shorter than four fields and counts unreadable ones. ScanResult has a kingdom field after objects. MapObject.area_positions

  • map: MapScanner.scan_kingdom / scan_chunks and MapService.scan_kingdom / scan_chunks default chunk_delay 0.2 -> 0.0.

  • map: NoobProtection -> KingdomProtection, and GetMapAreaResponse.noob_protection -> protection. kingdom_id is int | None.

  • map: OwnerCastlePosition (MapObject, MovementOwner and PlayerProfile castle_positions / village_positions) is a NamedTuple, not a BasePayload: model_dump() / model_validate() / to_payload() and extra keys are gone (use _asdict(), or OwnerCastlePosition(*row)); it compares equal to a plain tuple, and dumping its owner record gives tuples for AP and VP instead of dicts.

  • map,player: An owner record with a truthy DUM keeps only OID (name "", every other field at its default). SearchPlayerResponse.area is a MapArea (items, owners), not a GetMapAreaResponse. MapAreaItem has a new is_plot_row field that is_relocating reads.

  • messages: Messages.MESSAGE_TYPE_SPY_PLAYER and MESSAGE_TYPE_SPY_NPC are removed; use MessageType.SPY_PLAYER and MessageType.SPY_NPC. SpyLogHeader.area_name is decoded.

  • network: Concurrent request()/send(wait=True) callers of the same command are served one at a time, and time queued behind an earlier one counts against their timeout, so a caller can now time out without having sent anything. Test doubles of Connection.request must accept the new accepts keyword.

  • player: PlayerProfileBase fields renamed or retyped: special_ability -> is_searching_alliance (bool); resource_request_date -> relocation_remaining_seconds; vip_flag (int) -> has_vip_flag (bool); premium_flag (int) -> has_premium_flag (bool); AllianceMember.is_in_ruins -> is_ruin; alliance_id defaults to -1 instead of 0 (GetPlayerInfoResponse.alliance_id too); title_prefix and title_suffix default to None instead of 0 and -1; AllianceMember.name defaults to "" instead of "Unknown".

  • player: PlayerService.get_player_details_bulk returns a PlayerDetailsBulkResult instead of a dict (the old dict is its found); timeout is now the wait for each reply instead of for all of them (default 5.0 instead of 10.0), and the requests go out one at a time.

  • player: PlayerService.get_player_details_bulk send_delay default 0.05 -> 0.0.

  • player: SearchPlayerRequest (and search_player_by_name) sends PN encoded as chat text: ' as &145;, " as ", % as %, a backslash as %5C. A caller who encoded the name themselves must stop doing so.

  • pool: AccountPool(registry=None) -> AccountPool(registry); pass an AccountRegistry (load it with registry.load(file_path=...)). The pool no longer falls back to empire_core.accounts.accounts, and AccountPool.registry is a plain attribute.

  • protocol: GGECommand.ANI, GGECommand.GFC and GGECommand.SEM are removed.

  • protocol: GGEError.LOGIN_COOLDOWN is renamed to LOGIN_COOLDOWN_ACTIVE. empire_core.config.ServerError is removed; use GGEError.LOGIN_COOLDOWN_ACTIVE instead of ServerError.LOGIN_COOLDOWN.

  • protocol: Json_text raises ValueError for NaN and infinities, and TypeError for values JSON has no form for; floats are written as JavaScript writes them. build_command sends False as and True as "true".

  • protocol: Packet.build_xt(request_id=...) is renamed to room_id= and now defaults to -1 instead of 1, as does the new BaseRequest.to_packet(room_id=...). Frames built without a room id now carry -1; pass client.connection.room_id. Request JSON is compact and keeps non-ASCII characters, '%' in any string param is sent as '%' and "'" is removed.

  • protocol: Packet.iter_from_bytes is removed.

  • protocol: Packet.to_bytes() no longer appends "\0".

  • protocol: Send()/request_packet() for gaa, ssi, ain, grc, dfc, mcm, jaa (by position), csm, cra, cds and cat no longer return a successful reply that names another request; they wait for the matching one and raise EmpireTimeoutError if none comes. A gaa reply without KID that has rows is refused.

  • ranking: League_type_id no longer defaults to -1 on RankingService.get_ranking_list, get_own_ranking_page and get_ranking_window, nor on GetRankingListRequest (llsp) and GetRankingWindowRequest (llsw); pass client.events.get_league_id(event_id) for the event's league, or -1 for the donation ranking.

  • services: Empire_core.services.register_service, get_registered_services, the registry behind them, BaseService._service_name and EmpireClient._services are removed. A service is added by building it in EmpireClient._attach_services().

  • spy: SpyProtection is removed; SpyTargetArea.protection is a KingdomProtection. beginner_protection_seconds -> noob_protection_seconds, is_beginner_protected -> is_noob_protected, faction_protection_status -> protection_status, faction_protection_seconds (int | None) -> protection_seconds (int, 0 when absent).

  • spy: SpyService.execute_instant_spy no longer waits for spies by default; pass wait_for_spies=8 for the previous behaviour.

  • spy,messages: Renamed messages models: BattleSpyDataRequest -> GetSpyReportRequest, BattleSpyDataResponse -> SpyReportResponse, SpyCastleInfo -> SpyReportArea (castle_name -> name; KL/WL/GL/TL/ML default 0 instead of -1, as int(undefined) in the client; area_type, x and y are required instead of defaulting to -1, area_type a MapItemType; kingdom is a Kingdom defaulting to Kingdom.GREEN instead of -1). BattleSpyDataResponse.target -> SpyReportResponse.area. SpyReportResponse.message_id defaults to None instead of 0. empire_core.SpyCastleInfo is gone; empire_core exports SpyReportArea, SpyReportResponse, SpyOutcome and SpyStep. SendSpyRequest.precision -> accuracy_or_damage; pay_to_travel 1 now sends horses_type -1. SpyResult fields success, reason, spy_data, defending_castellan, target and army -> outcome, step, error, message_id, report, mission (success and army are properties; the report's fields are on report): reason "sne_timeout" -> SpyOutcome.TIMEOUT, "ssi_failed_"/"csm_failed_"/"bsd_failed_*" -> COMMAND_FAILED with step and error, bsd error 130 or 66 -> NO_SPY_DATA, the others -> the SpyOutcome member of the same name. NPC targets are costed without the 5% floor, so execute_instant_spy can send a different number of spies. SpyScreenInfoResponse counts are read through int(). ForwardSpyLogRequest's payload is {"MID", "PID"} in that order. forward_report returns True for error 167.

  • state: After an unexpected disconnect or close(), client.state holds no player, castles or movements until the next login refills it; previously the old session's data stayed readable.

  • state: State callbacks no longer run concurrently; a callback that blocks (waiting for a reply, for example) now delays every callback queued behind it. Hand long work to another thread.

v0.41.0 (2026-09-30)

Bug Fixes

  • alliance: Read the alliance chat log from CM as the client does (#235, c28e42e)

  • army: Read RUT arrays and odd gcu values as the client does, and keep bup off the hospital (#218, c665907)

  • army: Send army and hospital commands as the client's command objects do (#218, c665907)

  • attack: Keep cra owner records by the client's truthiness of OID (#226, 7db5537)

  • attack: Require the commander on CreateAttackRequest (#220, c4d5791)

  • castle: Keep a listed castle without a name, and leave out quietly the rows that name no castle (#230, fbc8b75)

  • castle,movements: Send arc in the client's order and encoding, and gam with no castle (#215, aae83e4)

  • enums: Drop enum members the client does not define (#223, 37bb461)

  • enums: Make the enums match the client's constants (#223, 37bb461)

  • gamedata: A league row without an event id matches no event (#228, a755b53)

  • gamedata: Give falsy values the client's default, and read the new tables with parseInt (#219, 79957ed)

  • gamedata: Keep a cached level of 0 instead of reading it as missing (#224, 825b77f)

  • gamedata: Load the default game data once, and back off after a failed load (#231, 266e006)

  • gamedata: Load the id enums lazily, and guard the generator against the cache file (#224, 825b77f)

  • gamedata: Name researches and construction items by group and level, and report name changes (#231, 266e006)

  • gamedata: Quote generated strings the way ruff format does (#231, 266e006)

  • gamedata: Raise a remembered load failure without its old traceback, and open the ids PR when the branch is already pushed (#231, 266e006)

  • gamedata: Read unit and tool rows with the client's keys, types and defaults (#219, 79957ed)

  • models: Build rst and csm as the client does (#230, fbc8b75)

  • models: Read each castle list row by its area type's layout (#230, fbc8b75)

  • protocol: Read an int too large for a JS number as none, and say where the row readers are more lenient than the client (#235, c28e42e)

  • protocol: Read the refer-a-friend flag through parseInt, and warn only on unreadable rows (#226, 7db5537)

  • ranking: Take the highscore list types from the client, and call LID the league type id (#228, a755b53)

  • spy: Check a caught report's position too, and take the payment options by keyword (#236, a76006b)

  • spy: Pay for a spy mission only when asked (#236, a76006b)

  • spy: Read the csm reply's movement from A as the client does (#236, a76006b)

  • spy: Wait for this mission's own report, and name the paid options as the other senders do (#236, a76006b)

  • spy,defense: Fix what a live run of the stack found (#236, a76006b)

  • types: Key the camp offset and landmark level tables by their enums (#225, 47deee9)

Build System

  • release: List every breaking change in the changelog and release notes (#232, 510c220)

Documentation

  • Describe the research names and the name-change list (#231, 266e006)

  • Keep client references out of Flank's summary and spy_type's description (#223, 37bb461)

  • Name where every runtime id input comes from, and require the attack commander (#220, c4d5791)

  • gamedata: Say which columns the id enum members keep, and that GameData keeps the raw rows of the id tables (#231, 266e006)

  • gamedata: Show GameData.record in the GameData docstring and the README imports (#231, 266e006)

  • models: Describe where each request's runtime ids come from (#220, c4d5791)

  • models: Keep field descriptions to what each field is (#225, 47deee9)

  • models: Say where castle resources come from, and point building ids at BuildResponse (#225, 47deee9)

  • ranking: Say llsp's LID is the event's league, or -1 (#236, a76006b)

  • ranking,alliance: Say llsp pages event leaderboards and that the server answers acl (#236, a76006b)

  • services: Name the call that returns each runtime id a method takes (#220, c4d5791)

Features

  • defense: Read your own castle's defense with dfc, and name sdi for support (#236, a76006b)

  • enums: Add Flank.REINFORCEMENT_SUMMARY and cite the client's constants (#223, 37bb461)

  • enums: Type attack, spy, castle and map inputs with the client's constants (#216, 675b87f)

  • enums: Type finite inputs with the client's constants (#216, 675b87f)

  • gamedata: Carry each id's fixed data and link it to the loaded game data (#231, 266e006)

  • gamedata: Generate enums for buildings, researches, construction items, events and more (#231, 266e006)

  • gamedata: Generate id enums from the items data (#224, 825b77f)

  • gamedata: Give the id enums their rows' fixed data, and look full records up on GameData (#231, 266e006)

  • gamedata: Look up game-data rows by their unique keys (#217, 90fe2d3)

Refactoring

  • Consolidate duplicated helpers (#226, 7db5537)

  • Import models from their area packages and move the enums up (#233, 0c4ad8f)

  • Merge the commander services into commanders.service (#233, 0c4ad8f)

  • Move models and services into per-area packages (renames only) (#233, 0c4ad8f)

  • Move the owner records from movements to map.models.owners (#233, 0c4ad8f)

  • Name currency 1 and 2 coins and rubies (#227, e1a20f3)

  • Organise the library by game area (#233, 0c4ad8f)

  • Split the alliance models into info, help and search (#233, 0c4ad8f)

  • Split the army models into units, production and hospital (#233, 0c4ad8f)

  • Split the attack models by command group, waves to army, spy commands to spy (#233, 0c4ad8f)

  • Split the castle models into castles, details and actions (#233, 0c4ad8f)

  • Split the equipment models out of the commander roster (#233, 0c4ad8f)

  • Split the map models into items and areas (#233, 0c4ad8f)

  • Take Kingdom, not Kingdom | int, and build the touched models to spec (#230, fbc8b75)

  • alliance: Drop the chat helpers that duplicate AllianceService (#234, 8f0bd0a)

  • castle: Type the jca and arc kingdom and area type inputs, and describe jca (#216, 675b87f)

  • client: Reach the area helpers through their services (#234, 8f0bd0a)

  • combat: Name effect types with CombatEffectType (#223, 37bb461)

  • enums: Keep every enum in the utils.enums package (#222, af04c1d)

  • enums: One enum per id space, with WearerType as the client has it (#216, 675b87f)

  • enums: Spell members as the client does where only the spelling differed (#223, 37bb461)

  • gamedata: Drop the implicit game data behind the id enums, and look records up on GameData (#231, 266e006)

  • map: Type the client the map scanner uses (#234, 8f0bd0a)

  • models: Compare with enums instead of raw ids and labels (#223, 37bb461)

  • models: Drop the hand-written location type names (#230, fbc8b75)

  • models: Share the gcu model, type the army replies with it, and drop ProductionListType (#218, c665907)

  • models: Skip unreadable rows and blocks with one set of helpers (#226, 7db5537)

  • movements: Move the tracked Movement into the movements area (#234, 8f0bd0a)

  • protocol: Encode and decode text only as the client does (#226, 7db5537)

  • protocol: Keep the client's JavaScript conversions in one module (#226, 7db5537)

  • services: Look an own castle up in the state in one place (#226, 7db5537)

  • types: Take Kingdom, not Kingdom | int (#230, fbc8b75)

Testing

  • Enforce the area layering, and point the docs at the new layout (#233, 0c4ad8f)

  • Forget the default game data after every test (#231, 266e006)

  • Mirror the area packages in the test tree (#233, 0c4ad8f)

  • attack: Give the cra enum test its commander so it can fail (#220, c4d5791)

  • gamedata: Keep CombatEffectType's ids within the generated EffectType (#224, 825b77f)

  • gamedata: Read row models out of generic annotations on Python 3.10 (#217, 90fe2d3)

  • map: Watch the map areas logger that reports skipped area rows (#233, 0c4ad8f)

  • spy: Make the owner record's level, might, achievement points and crest colours up (#236, a76006b)

  • state: Name the no-gcu test after coins and rubies (#227, e1a20f3)

Breaking Changes

  • Deep module paths moved. Import from empire_core, empire_core.protocol.models or the area packages instead. Removed paths, with where their contents live after this series:

  • Renamed public names: AutoSkipCooldownType.C2 -> AutoSkipCooldownType.RUBIES; UnitStats.healing_cost_c1 -> healing_cost_coins; UnitStats.healing_cost_c2 -> healing_cost_rubies; AttackSlotDef.cost_c2 -> cost_rubies; FillOptions.allow_c1_cost -> allow_coin_cost; FillOptions.allow_c2_cost -> allow_ruby_cost; CurrencyTotals.gold -> coins; Player.gold -> coins; PlayerSnapshot.gold -> coins (sqlite column player_snapshots.gold -> coins); AllianceMemberInfo.given_c1 -> given_coins; AllianceMemberInfo.given_c2 -> given_rubies; AllianceStorage.coins1 -> coins, coins2 -> rubies, food -> oil, gold -> glass, coins -> coal; GGEError.NOT_ENOUGH_CURRENCY1 -> NOT_ENOUGH_COINS; GGEError.NOT_ENOUGH_CURRENCY2 -> NOT_ENOUGH_RUBIES; GGEError.ALLI_NOT_ENOUGH_C1 -> ALLI_NOT_ENOUGH_COINS; GGEError.ALLI_NOT_ENOUGH_C2 -> ALLI_NOT_ENOUGH_RUBIES; GGEError.WRONG_AMOUNT_OF_BOUGHT_C2 -> WRONG_AMOUNT_OF_BOUGHT_RUBIES; GGEError.C2_CONFIRMATION_REQUIRED -> RUBY_CONFIRMATION_REQUIRED

  • alliance: ChatLogEntry is removed; AllianceChatLogResponse.chat_log and AllianceService.get_chat_log now hold ChatMessageData, which has no timestamp field (use age_seconds). chat_log reads the "CM" key, not "CL". ChatMessageData's PID, PN and MT are no longer required: missing ones read as 0 and "".

  • army: Army request fields and service signatures follow the client. Renamed: DeleteUnitsRequest/Response -> DismissUnitsRequest/Response, DeleteWoundedRequest/Response -> DismissWoundedRequest/Response, GetProductionQueueRequest/Response -> GetProductionListRequest/Response, ProductionQueueItem -> ProductionSlot (plus CurrentProductionSlot and HospitalSlot), DoubleProductionRequest/Response -> DoubleProductionSlotRequest/Response, ArmyService.delete_units -> dismiss_units, get_production_queue -> get_production_list, skip_heal_time -> skip_heal. New: ProductionListId, SlotType, DismissManyWoundedRequest, WoundedUnits, AddedUnit, ProductionList, BUY_UNIT_PACKAGE_SK, ArmyService.dismiss_wounded_units. produce_units, get_production_list, cancel_production and double_production_slot take a ProductionListId (and slot type and position) instead of building and queue ids; unit methods take wod_id/amount; cancel_heal and skip_heal take a slot position; heal_all takes ruby_cost and returns bool instead of an invented healed count. The old reply fields (QID, CT, RS, UH, Q) are gone.

  • attack: CreateAttackRequest.commander_id (LID) has no default.

  • castle: RenameCastleRequest.kingdom_id takes Kingdom | int and castle_type MapItemType | int.

  • castle,movements: GetMovementsRequest has no castle_id. RenameCastleRequest sends its keys in the client's order and encodes the name.

  • client: The game-area helpers are no longer EmpireClient methods. Old -> new:

  • combat: The effect type constants ATTACK_BONUS_UNIT_TYPE, UNLOCK_ABILITY_EFFECT_TYPE (combat.bonuses), TOOL_DEFENSE_BONUS_TYPE, DEFENDER_WALL_BONUS_TYPE, DEFENDER_GATE_BONUS_TYPE, DEFENDER_MOAT_BONUS_TYPE, DEFENSE_BONUS_TYPE, DEFENSE_BOOST_YARD_TYPE, DEFENSE_BOOST_FRONT_TYPE, DEFENSE_BOOST_FLANK_TYPE (combat.defense), MELEE_DEFENSE_MALUS_TYPE and RANGE_DEFENSE_MALUS_TYPE (combat.tools, also exported from empire_core.combat) are gone; use the CombatEffectType members of the same ids.

  • defense: DefenseService.get_castle_defense is renamed get_support_defense_info.

  • enums: MapItemType.METRO, ABG_RESOURCE_TOWER and ABG_TOWER, AttackType.KINGS_TOWER_CONQUER and CombatEffectType.REINFORCEMENT_BONUS and REINFORCEMENT_BOOST are renamed as above.

  • enums: MapItemType.ROBBER_BARON, EXTERNAL_KINGDOM, KHAN_CAMP and KHAN_TENT are gone; use DUNGEON, KINGDOM_CASTLE and ALLIANCE_NOMAD_CAMP. MovementType.UNKNOWN is gone, and Movement.movement_type_enum returns MovementType | None.

  • enums: MapObjectType and KingdomType are removed (use MapItemType and Kingdom), as are the MapObjectType is_player/is_npc/is_event/is_resource groupings and its UNKNOWN member; Movement.target_type_enum returns MapItemType | None. ISLAND_KINGDOM_ID is removed (use Kingdom.STORM). WearerType.ALL is removed in favour of WearerType.UNDEFINED (-1), and Equipment.wearer_type defaults to -1 instead of 0.

  • enums: Send_attack's attack_type and loot_priority, and get_attack_info's area_type, are now enums; CreateAttackRequest's ATT, LP and ASCT and SendSpyRequest's ST reject values the client does not define, and kingdom inputs are typed Kingdom | int.

  • enums: The enums are no longer importable from their old modules:

  • gamedata: UnitStats.level defaults to -1 instead of 0, so a row without a level no longer matches unit(type, 0) or tool(type, 0); pass level=-1 for those. ToolStats.category defaults to "0" instead of "". ToolStats.tool_category is lowercased ("basic", not "Basic"). UnitStats.loot_value and the ToolStats raw_*_bonus fields are int, not float. UnitStats.might_value, ToolStats.delete_after_battle and ToolStats.is_consumed_in_battle are removed. ToolStats.allowed_targets is typed tuple[tuple[int | None, int | None], ...].

  • models: CurrencyTotals is imported from protocol.models.base (still re-exported from protocol.models); the army replies' currencies are CurrencyTotals instead of dicts; ProductionListType is removed, use ProductionListId.

  • models: Gcl entries in a block whose KID is not a Kingdom are dropped from GetCastlesResponse and state.castles. PlayerCastle.from_list raises ValueError (ValidationError for a field of the wrong type) on a short row, an area type a castle list does not hold or bad field types, where it used to return an object; its location_id and name are None for a faction capital, which has no CastleInfo. PlayerCastle.capturer_id, CastleInfo.occupier_id and get_location_captures now include main and kingdom castles, and count occupier 0. CastleInfo and PlayerCastle level fields are int | None (None for landmarks) and castle rows' levels are floored as the client does. RenameCastleRequest.castle_type no longer takes an int outside MapItemType.

  • models: LOCATION_TYPES, get_location_type_name, PlayerCastle.castle_type_name and LocationCapture.location_type_name are removed; use MapItemType(...).name. castle_type and location_type are MapItemType, and CastleInfo.occupier_id of a castle or kingdom castle is read from its row instead of always being -1.

  • models: RelocateCastleRequest takes PX and PY only (castle_id, kingdom_id and the X/Y aliases are gone), SendSpyRequest.sd is renamed slowdown, and csm is sent in the client's key order.

  • models: RelocateCastleRequest.kingdom_id is typed Kingdom | int and defaults to Kingdom.GREEN.

  • movements: Empire_core.state.world_models is now empire_core.movements.tracked, and its logger is empire_core.movements.tracked. empire_core.Movement and empire_core.MovementResources are unchanged.

  • protocol: Client_int, ClientInt, parse_int and ParseInt are no longer in empire_core.protocol.models.base. Import them from empire_core.protocol.js; client_int is now js_int and parse_int is js_parse_int_or_zero.

  • protocol: Encode_chat_text and decode_chat_text are removed from empire_core, empire_core.protocol.models and its base and chat modules. Use encode_json_text and decode_json_text from empire_core (or empire_core.protocol.text). smartfox_json_text and parse_chat_json_message are no longer in empire_core.protocol.models.base.

  • ranking: RankingType members renamed: ACHIEVEMENTS -> PLAYER_ACHIEVEMENT_POINTS, PLAYER_MIGHT -> PLAYER_MIGHT_POINTS, LEGEND_LEVEL -> PLAYER_LEGEND, ALLIANCE_MIGHT -> ALLIANCE_MIGHT_POINTS, DOMINION_POINTS -> ALLIANCE_LANDMARKS, CARGO_POINTS -> ALLIANCE_AQUA_POINTS. Removed, as the client has no such list at those values: FOREIGN_INVASION=71 (use ALIEN_INVASION, ALLIANCE_ALIEN_INVASION_PLAYER or ALLIANCE_ALIEN_INVASION_ALLIANCE), BLOODCROWS=72 (use ALLIANCE_RED_ALIEN_INVASION_PLAYER or ALLIANCE_RED_ALIEN_INVASION_ALLIANCE), SAMURAI=80 (use SAMURAI_PLAYER or SAMURAI_ALLIANCE), NOMAD=85 (use NOMADINVASION, ALLIANCE_NOMADINVASION_PLAYER or ALLIANCE_NOMADINVASION_ALLIANCE), OUTER_REALMS=63 (63 is KINGDOMS_LEAGUE_SEASON), BERIMOND=113, SHAPESHIFTER=60 and HORIZON=134. RankingCategory is removed. list_id -> league_type_id on GetHighscoreRequest, GetHighscoreResponse, GetRankingListRequest, GetRankingListResponse and SearchAllianceRequest, and on RankingService.get_highscore/get_ranking_list. GetHighscoreRequest and GetRankingListRequest default it to -1 and always send it; GetHighscoreResponse.league_type_id is -1 rather than None when missing. GetRankingListRequest requires max_results (M) and defaults rank to 1; get_ranking_list takes max_results as its third argument. LeagueBracketDef.league_id -> league_type_id. The list type (LT) of GetHighscoreRequest, GetRankingListRequest, SearchAllianceRequest and RankingService.get_highscore/get_ranking_list takes a RankingType instead of an int.

  • spy: Execute_instant_spy no longer pays with feathers by default; pass feathers=True for the old behaviour.

  • spy: SendSpyResponse no longer has the arrival_time field; movement_id is a property that is None when the reply has no movement, and the movement is spy_movement.

  • spy: The invalid_sne_format and sne_timeout_or_error_* reasons are gone: unreadable or unrelated sne messages are skipped, and a wait that ends without the report gives sne_timeout (or report_target_mismatch when only reports for another position came, disconnected when the connection dropped).

  • types: Kingdom parameters and fields are typed Kingdom. An int naming one of the six kingdoms still validates, but any other id is now a ValidationError (ValueError from fill_attack), GetMapAreaResponse with an unknown KID fails validation, camp_kingdom_id returns Kingdom | None, and GetDefenseRequest.kingdom_id defaults to None instead of -1.

v0.40.0 (2026-09-29)

Bug Fixes

  • alliance: Decode ain text exactly as parseChatJSONMessage does (#204, b08d0a7)

  • alliance: Drop the duplicate SRFU/HRFU fields and read ain text as the client does (#204, b08d0a7)

  • alliance: Read the parseInt fields of ain as parseInt does (#204, b08d0a7)

  • attack: Fill from the pre-calculation's army, and join the castle before gui (#211, c990e47)

  • attack: Fill from the stronghold units too, and refuse to read an unjoined castle (#211, c990e47)

  • combat: Add the bonuses of the gems slotted in a commander's equipment (#205, 64d43fe)

  • combat: Count a commander's alien equipment bonuses (#206, 6d3742e)

  • combat: Count alien equipment gems and add bonuses in the client's order (#206, 6d3742e)

  • combat: Count worn items in the client's slot order, one per slot (#206, 6d3742e)

  • combat: Read the value, not the wod id, of effect types 47, 51, 168 and 214 (#205, 64d43fe)

  • combat: Resolve commander equipment bonuses and gems as the client does (#205, 64d43fe)

  • combat: Resolve equipment bonus ids through the equipment effect table (#205, 64d43fe)

  • commanders: Keep an item whose set id or rarity is odd (#206, 6d3742e)

  • gamedata: Read equipment effect rows with the client's defaults, and describe them (#205, 64d43fe)

  • skills: Keep RS fractions as parse_SKL does, and describe the id list rule (#207, 398eec2)

  • skills: Read generals and skills as the client does, and add the general commands (#207, 398eec2)

  • skills: Read null skill lists as no skills, and document the general commands (#207, 398eec2)

  • skills: Read the general's flags, old xp and fixed level as the client does (#207, 398eec2)

Documentation

  • alliance: Describe every AllianceInfo field and read AID as parseInt does (#204, b08d0a7)

  • commanders: Describe every equipment and commander field and cite the gli classes (#206, 6d3742e)

  • skills: Describe every general and skill field and cite the gie and skl classes (#207, 398eec2)

Features

  • commanders: Add the arl command to rename a commander or castellan (#206, 6d3742e)

  • commanders: Keep the hero alien string, add UNIQUE_TEMPORARY and has_set (#206, 6d3742e)

  • commanders: Read LICID, AIE/TAE/GEM and a castellan's movement availability (#206, 6d3742e)

  • commanders: Read LICID, alien equipment and hero fields, and rename commanders (#206, 6d3742e)

  • equipment: Read the equipment inventory and equip or unequip items (#208, 4de96af)

  • skills: Add the commands that change generals (#207, 398eec2)

  • skills: Read the reset counter and the sceat skills being activated (#207, 398eec2)

Refactoring

Breaking Changes

  • alliance: AllianceInfo.spend_resources_food_upgrade and help_resources_food_upgrade are removed; use soft_relic_forge_uses and hard_relic_forge_uses. description and announcement are decoded, and an empty announcement reads as " ".

  • army: GetUnitsResponse.inventory is renamed units and, with in_production, stronghold and hospital, holds {wod_id: amount} instead of [wod_id, amount] lists; the units (U) and tools (T) fields are removed.

  • attack: GetUnitsRequest has no castle_id and sends {}; army.get_units and get_units_response join the given castle before reading it.

  • combat: Commander_bonuses takes the GameData as its first argument: commander_bonuses(game_data, commander, area_effects=...).

  • skills: General.fixed_level is -1 for L: 0 (was 0), and General.star_level is derived from L when ST is missing or 0 and L is a multiple of 10 (was 0).

v0.39.0 (2026-09-25)

Bug Fixes

  • alliance: Name the ain settings and owner record points as the client reads them (#203, cceab23)

  • army: Send_support no longer uses the premium commander or feathers by default (#195, d4f6890)

  • attack: BPC is the premium commander on attacks too, and supports need a commander (#195, d4f6890)

  • attack: Keep the currency, owner and attack-in-progress data of a cra reply (#192, b8e7f20)

  • attack: Keep the pre-calculation when the tile scan fails, drop treasure dungeons (#191, 5bc2157)

  • attack: Leave spied amounts of 0 or less out, as the client does (#203, cceab23)

  • attack: Model msd and sdc as the dungeon cooldown skips they are (#193, ea54062)

  • attack: No defender legend skills without a spy report (#190, 9eed1b5)

  • attack: Parse a dungeon skip's reply row as a map area (#193, ea54062)

  • attack: Raise ATTACK_IN_PROGRESS with its details, and name FC send_anyway (#192, b8e7f20)

  • attack: Read attack presets in the client's shape and allow saving them (#194, 99d0cca)

  • attack: Read the spy report age and the castellan from aci like the client (#190, 9eed1b5)

  • attack: Send adi's keys in the client's order (#191, 5bc2157)

  • attack: Send attack waves and support in the client's shape (#195, d4f6890)

  • attack: Take the defender's legend skills from the pre-calculation (#201, 26a10a1)

  • castle: Log gcl kingdom entries that cannot be read (#203, cceab23)

  • combat: Add legend skills, support tools and extra waves to the attacker (#199, 0fc7649)

  • combat: Apply the client's three legendary-fight rules (#197, de689d8)

  • combat: Count defense tools and defender legend skills in a spied defense (#201, 26a10a1)

  • combat: Faction camp owners, conquer control, and the owner's legend level from aci (#197, de689d8)

  • combat: Merge the flank bonus sources before truncating (#198, 68aa0b4)

  • combat: Refuse raid-boss tools when their boss is not active (#200, 17dd902)

  • defense: Import Any for the dfc castellan validators (#203, cceab23)

  • defense: Keep a dfc reply whose castellan cannot be read (#196, 43b768c)

  • defense: Send the castle defense commands in the client's shape (#196, 43b768c)

  • defense: Type the dfc castellan and unit inventory (#196, 43b768c)

  • map: Read structure levels only from rows that carry them (#203, cceab23)

  • models: Keep owner records, map rows and leaderboards as lenient as the client (#203, cceab23)

  • models: Read commanders, owner records, cra and sne as leniently as the client (#203, cceab23)

  • player: Fold gdi's landmark lists into its castles, and find a wsp player at X/Y (#203, cceab23)

  • ranking: Read llsp rows as leniently as the client (#203, cceab23)

Documentation

  • messages: Describe bsd as the spy report it is (#203, cceab23)

Features

  • attack: Pre-calculate each kind of target with the command the client uses (#191, 5bc2157)

  • commanders: Model a relic item's type, might and gem (#203, cceab23)

Refactoring

  • alliance: Type the member info, diplomacy, landmark, bookmark and search lists (#203, cceab23)

  • attack: Type the gaa, gui and gli blocks of a pre-calculation reply (#203, cceab23)

  • attack: Type the movement, owners and currencies of a cra reply (#203, cceab23)

  • attack: Type the spied castellan of a pre-calculation reply (#203, cceab23)

  • attack: Type the spy positions and area effects of a pre-calculation reply (#203, cceab23)

  • commanders: Type the effects, equipment and item bonuses of a commander (#203, cceab23)

  • defense: Type the spy positions, castellan, gui and gli of an sdi reply (#203, cceab23)

  • gamedata: Keep a tool's effects as the string the client splits (#203, cceab23)

  • map: Parse a map area's rows into items at parse time (#203, cceab23)

  • messages: Type the sne messages and the bsd spy report (#203, cceab23)

  • models: Type the crest, faction and castle lists of owner records (#203, cceab23)

  • models: Type the wire data models hold as raw dicts and lists (#203, cceab23)

  • movements: Type the area rows and the commander of a movement (#203, cceab23)

  • player: Type the gcl block of gdi and the gaa block of wsp (#203, cceab23)

  • ranking: Type the llsp leaderboard entries (#203, cceab23)

  • skills: Type a general's selected abilities (#203, cceab23)

Testing

  • attack: Give the stubbed pre-calculation its owner records (#201, 26a10a1)

Breaking Changes

  • alliance: AllianceInfo.castle_count, total_castles, homepage, invite_only, ignore_applications, kick_applications, abbreviation, friendly_raids, support_priority, auto_accept, alliance_war, member_limit, attack_protection, message_filter and invite_friends are renamed to fame_points, highest_fame_points, can_be_invited_to_hard_pact, is_searching_members, is_accepting_members, is_king_alliance, announcement, free_renames, can_be_invited_to_soft_pact, application_count, auto_war, external_member_level, aqua_points, is_able_to_forge and is_forge_inventory_full; required_trust is removed. PlayerProfileBase h_field, castle_count, total_castles and avatar_points are renamed to honor, glory_points, highest_glory_points and achievement_points.

  • alliance: AllianceInfo.member_info is list[AllianceMemberInfo]; alliance_diplomacy is list[AllianceDiplomacyStatus]; alliance_contracts, alliance_truces, alliance_kingdoms, alliance_monuments and alliance_landmarks are renamed to capitals, metropolises, kings_towers, monuments and laboratories (list[MapAreaItem]). AllianceBookmark.object_info and positions are replaced by owner, a MapObject. SearchAllianceResponse.raw_results is replaced by results; AllianceSearchResult.from_list and might are removed, and it gains rank, score and fame_points. A non-list AMI or L now reads as empty instead of failing validation, and a missing search name is "" instead of "Unknown".

  • army: Send_support's boost_with_coins is use_premium_commander (default False), feathers is a bool (default False) and commander_id defaults to 0. SendSupportRequest.boost_with_coins is use_premium_commander, and LID, BPC and PTT default to 0.

  • attack: AttackInfoResponse.raw_defending_castellan and raw_defending_castellan_fallback are replaced by spied_castellan and spied_castellan_fallback, which hold Commander models.

  • attack: AttackInfoResponse.raw_map_area, raw_inventory and raw_commanders are replaced by target_area, unit_inventory and commander_roster, and owner_records() returns MapObject records instead of dicts.

  • attack: AttackInfoResponse.raw_spy_army is renamed spy_data, and raw_attacker_effects is replaced by attacker_effects, a list of CommanderEffect models.

  • attack: CreateAttackResponse.attack_movement is a MovementWrapper instead of a dict, currencies a CurrencyTotals or None instead of a dict, and owners a list of MovementOwner models. CreateAttackRequest's and send_attack's collector_booster take a list of [currency_id, amount] pairs.

  • attack: Fill_wave and fill_waves return waves whose containers are padded with [-1, 0] for every empty or locked slot. SendSupportRequest no longer has kingdom_id and CastleService.send_support no longer takes it.

  • attack: GetAttackInfoResponse.defending_castellan_id is now spy_age_seconds. raw_defending_castellan now holds abe and the B entry moved to raw_defending_castellan_fallback. spy_army() and defending_castellan() return None when S is empty. target_row() no longer unwraps a nested row list.

  • attack: GetPresetsRequest takes no castle_id. AttackPreset now has index, name and raw_army with an army() decoder instead of preset_id, name, units and tools. GetPresetsResponse.presets reads the S key.

  • attack: GetTargetInfoRequest, GetTargetInfoResponse and TargetInfo are removed; use GetDungeonAttackInfoRequest and GetDungeonAttackInfoResponse. get_attack_info returns an AttackInfoResponse and takes timeout as a keyword after area_type and conquer. fill_attack on a target of unknown area type now scans the tile before the pre-calculation.

  • attack: Send_attack raises AttackInProgressError on ATTACK_IN_PROGRESS instead of returning False. CreateAttackRequest.fast_cast is send_anyway.

  • attack: Send_attack's boost_with_coins and CreateAttackRequest.boost_with_coins are use_premium_commander. send_support takes commander_id right after units, with no default, and SendSupportRequest.commander_id is required.

  • attack: SkipAttackCooldownRequest/Response are now MinuteSkipDungeonRequest/Response and SkipDefenseCooldownRequest/Response are now SkipDungeonCooldownRequest/Response. The requests take the dungeon's coordinates, kingdom and treasure-map ids instead of castle_id, and the replies expose area_row instead of rubies_spent.

  • combat: EffectDef.raid_boss_id is replaced by raw_raid_boss_ids and the raid_boss_ids property. can_use_tool_on_target now takes the game data as a third argument.

  • combat: Fill_waves and fill_attack no longer take sceat_skill_ids.

  • combat: Is_legendary_fight is removed; use LegendaryFight.evaluate. Without owner_id, fill_waves grants no legend skills except an alien camp's, whose owner id follows from its area type.

  • commanders: LeaderBase.raw_equipment and the equipment() method are replaced by the equipment field, a list of Equipment. LeaderBase.effects and area_effects hold CommanderEffect models instead of raw rows. Equipment.bonuses holds EquipmentBonus models, and a relic item's bonuses move to Equipment.relic_bonuses as RelicBonus models.

  • defense: GetDefenseRequest and the ChangeKeep/Wall/MoatDefenseRequest models take the castle's coordinates and area id instead of castle_id, and the change requests take slot lists instead of units and tools, with ChangeWallDefenseRequest taking one WallSectionSetup per section. GetDefenseResponse has the dfc fields instead of keep/wall/moat/courtyard. DefenseConfiguration is removed, and ChangeKeep/Wall/MoatDefenseResponse are replaced by KeepDefense, WallDefense and MoatDefense.

  • defense: GetSupportDefenseResponse.castellan_info (dict) is replaced by castellan (Castellan), unit_inventory is a UnitInventory instead of a dict, and commanders_info (dict) is replaced by commander_roster (CommanderRoster). defense_positions holds pairs already read through int().

  • gamedata: ToolStats.effects is renamed to raw_effects and is a str instead of Any.

  • map: GetMapAreaResponse.raw_items is removed; items is now a field holding the parsed rows, and unparseable rows are logged when the reply is parsed rather than when items is read.

  • map: MapAreaItem's structure levels are 0 for kings towers, monuments, laboratories, villages and isles, and a capital's or metropolis's keep, wall and gate levels are no longer floored at 1.

  • messages: SystemNotificationEvent.messages is list[MessageInfo] instead of raw lists. BattleSpyDataResponse.battle_data and SpyResult.battle_data are renamed to defending_castellan and are Castellan | None instead of a dict. BattleSpyDataResponse.spy_data and SpyResult.spy_data are list[list[list[int]]].

  • models: MemberEmblem and MemberCastle are removed. AllianceMember.emblem, PlayerOwnerInfo.emblem and MapObject.emblem are OwnerCrest (icon_style is now is_set, symbol_color1/2 are symbol1_color/symbol2_color). PlayerProfileBase.castle_positions and village_positions are list[OwnerCastlePosition] and the castles property is removed. MapObject.faction is OwnerFaction and MapObject.alliance_emblem is AllianceEmblem instead of dicts.

  • movements: Movement.target_area and Movement.source_area are MovementArea | None instead of the raw list (the row is at .row). Movement.commander_equipment is list[Equipment] and Movement.commander_effects is list[CommanderEffect] instead of raw lists. MovementUnitInfo.commander is Commander | None instead of a dict.

  • player: A gcl row wrapped in an extra list is no longer unwrapped and is skipped, as the client would not read it. SearchPlayerResponse gains x and y, and get_player picks the owner of the area at X/Y rather than the first owner record.

  • player: GetPlayerInfoResponse.raw_castle_list is replaced by castle_list (GetCastlesResponse) and get_castles returns list[CastleInfo] instead of list[PlayerCastle] (castle_name, kingdom_id, castle_id and occupier_id instead of name, kingdom, location_id and capturer_id). Like the gcl command it drops rows of ten fields or fewer, and a capture's kingdom is the list's KID. SearchPlayerResponse.raw_gaa is replaced by area, SearchPlayerResult is removed and get_player returns MapObject | None. PlayerCastle, LOCATION_TYPES and get_location_type_name are no longer importable from models.player; import them from models.castle or models.

  • ranking: GetRankingListResponse.raw_list is replaced by scores, a list[LeaderboardScore].

  • skills: General.raw_abilities is replaced by selected_abilities, a list[SelectedAbility]; ability_ids no longer includes -1 or bare ids.

v0.38.0 (2026-09-25)

Bug Fixes

  • state: Alliance alerts only for players, and the township counts as yours (#184, ec52eb7)

  • state: An unreadable LVL or XP keeps the previous value (#188, 6a0a193)

  • state: Apply player pushes after login, plus honor and protection (#187, 3ff527f)

  • state: Call the sce entries special currencies, not inventory (#189, c391575)

  • state: Compute legend level and level XP bounds like the client (#188, 6a0a193)

  • state: Copy beginner_protection in snapshots, skip unreadable pushes (#187, 3ff527f)

  • state: Drop the deprecated inventory names (#189, c391575)

  • state: Keep movement owner records and alert like the client (#184, ec52eb7)

  • state: Read area ids and names at the area type's own positions (#183, 470da9a)

  • state: Read every movement wrapper block the client reads (#182, c007a99)

  • state: Store the movements you send from the send replies (#186, 794e132)

  • state: The deprecated Player.inventory can be assigned again (#189, c391575)

Breaking Changes

  • state: MovementResources.ash is aquamarine. Movement.resources is filled from market goods or travel loot, never from GS.

  • state: On_incoming_attack no longer fires before the local player is known, nor for attacks whose target is outside your alliance.

  • state: Player.inventory, Player(inventory=...) and GameState.get_inventory() are gone. Use Player.special_currencies and GameState.get_special_currencies().

  • state: Player.LVL, XP, LL, XPFCL and XPTNL are replaced by level, xp, legendary_level, xp_for_current_level and xp_to_next_level, which are now fields (they were read-only properties).

  • state: Player.model_dump() now uses the key special_currencies; there is no inventory key any more. Player(inventory=...) and the inventory property still work, with a DeprecationWarning.

v0.37.0 (2026-09-25)

Bug Fixes

  • protocol: Model gam as wrappers around a movement record (#177, 14deeb9)

  • protocol: Type every gam block from the client's parsers (#177, 14deeb9)

  • state: Fill castles from the whole dcl entry, with the client's names (#179, 65ae161)

  • state: Read the alliance search flag and fame from gal (#178, 8fd8861)

Code Style

  • protocol: Drop comments that restate the field descriptions (#177, 14deeb9)

  • state: Drop a comment that restates the validator (#178, 8fd8861)

  • state: Shorten a fixture comment (#179, 65ae161)

Refactoring

  • Point docs, comments and time patches at the modules that moved (#181, f4a2f53)

  • Split GameState and the gam models by area (#181, f4a2f53)

  • protocol: Call the UM lord a commander (#177, 14deeb9)

  • protocol: Move the gam models out of map.py (#181, f4a2f53)

  • state: Split GameState into per-area mixins (#181, f4a2f53)

Testing

  • Split the state and gam model tests by area (#181, f4a2f53)

  • protocol: Type the gam fixture for mypy (#177, 14deeb9)

Breaking Changes

  • protocol: Protocol.models.map.Movement is removed. GetMovementsResponse.movements holds MovementWrapper entries; the record is .movement, with the fields of the state Movement.

  • state: Alliance.abbreviation is removed; is_searching is the flag SA really holds. The AID, N, SA and R attributes are replaced by id, name, is_searching and rank.

  • state: Castle.max_castellans, has_workshop, has_dwelling, has_harbour and next_day_population are replaced by market_carriages, has_siege_workshop, has_defense_workshop, has_hospital and neutral_deco_points; defence and stronghold_units are new. Resources.ash is aquamarine. The OID, N, KID, X, Y, P, NDP, MC, B, WS, DW and H attributes are gone: use id, name, kingdom_id, x, y and the properties above.

v0.36.0 (2026-09-24)

Bug Fixes

  • state: Arrive by time, route abr/asr/mcm/mfc, mrm is a removal (#161, 8673d4c)

  • state: Movement types and direction from the game client (#160, 424a5ea)

  • state: Number movement types the way the game client does (#160, 424a5ea)

  • state: Tell incoming from outgoing by owner, returns by D (#160, 424a5ea)

  • state: Time-based arrival, abr/asr/mcm/mfc, mrm as removal (#161, 8673d4c)

Documentation

  • state: A return home arrives as a new TRAVEL movement (#160, 424a5ea)

  • state: Say which movements alert and that returns arrive too (#161, 8673d4c)

Refactoring

  • state: Snake_case Movement fields aliased to their wire keys (#161, 8673d4c)

Breaking Changes

  • state: Every MovementType member except SPY changed number or name. SUPPORT is DEFENCE, TRANSPORT is MARKET, and RAID, SETTLE, CAMP, TRADE, ATTACK_CAMP, RAID_CAMP and RETURN are removed.

  • state: Is_incoming and is_outgoing need the local player id and no longer read D. is_returning is D == 1. get_outgoing_movements() lists your armies heading out, not armies on their way home. is_transport is a market transport only; troops moved between your own castles are is_travel.

  • state: Movement no longer has the MID, T, PT, TT, D, TID, KID, SID, OID and HBW attributes. Use movement_id, movement_type, progress_time, total_time, direction, target_id, kingdom_id, source_id, owner_id and horse_booster_id, which keep their names and meanings.

  • state: On_movement_arrived is time-based and can fire from a query; STALE_MOVEMENT_GRACE is gone. on_movement_recalled no longer fires on mrm: use on_movement_removed for removals.

v0.35.3 (2026-09-24)

Bug Fixes

  • map: Tidy owner records and cover them with a live-shaped test (64e2095)

v0.35.2 (2026-09-24)

Bug Fixes

  • castle: Look up a castle's type and kingdom when renaming it (04fd1f3)

v0.35.1 (2026-09-23)

Bug Fixes

  • ranking: Retain response and entry metadata (d2338e9)

Documentation

  • Add contact details to the README (58abfb8)

v0.35.0 (2026-09-22)

Bug Fixes

  • castle: Read gcl and dcl in the shape the server sends (3210f47)

  • map: A row's owner is its player id for every owned type (4033349)

  • map: An unclaimed outpost has no owner (4010955)

  • map: Only rows that carry an owner report one (e35edee)

Features

  • castle: Model every gcl and dcl field the game client reads (ace984b)

  • castle: Typed storage and production from dcl, kingdom from the list (df2d0fb)

Refactoring

  • castle: Alias each positional gcl row field to its index (a4d20b8)

  • castle: Alias every gpa per-resource key (bddfea7)

  • castle: One aliased field per dcl wire key, derived rates as properties (ea7a8b5)

Testing

  • castle: Narrow the optional castle before reading its units (8d79e0c)

Breaking Changes

  • castle: CastleInfo lost level and gained owner_id. DetailedCastleInfo lost buildings, population and max_population; items is now units. BuildingInfo is gone. GetDetailedCastleRequest no longer takes a castle id. GetCastlesResponse and GetDetailedCastleResponse carry player_id and a castles list.

  • map: MapAreaItem.owner_id is the player id for type-1 and type-12 rows too. Use the new location_id for the castle id.

v0.34.0 (2026-09-09)

Bug Fixes

  • attack: Buff the courtyard's units too (3219224)

  • attack: Fall back to the map when the pre-calculation is refused (f6f1f01)

  • attack: Return to the attacking castle after scanning (cff2d70)

  • attack: Say which of three things left a target with no level (9399a4b)

  • attack: The commander's own equipment widens the flanks (16b8e70)

  • combat: A daimyo township is not a camp (a8a27e8)

  • combat: A row too short to name its camp is not a camp (678986f)

  • combat: A unit with an empty attack column stays at zero (d749e95)

  • combat: Act on the review's confirmed findings (bc5bbf2)

  • combat: Apply every condition an effect carries (4af43b3)

  • combat: Budget a tool per wave, not per flank (8d811a5)

  • combat: Feed a tool's effect malus back into the reductions (2e11329)

  • combat: Gate legend skills on a legendary fight, and drop the invented cap (4fd20f5)

  • combat: Only the middle flank meets the gate (11215d4)

  • combat: Read a keyed effect's value, not its key (b8667d9)

  • combat: Scale the flanks and the middle by their own bonuses (9161e9b)

  • combat: Size a wave by the level its target defends at (bdf48b6)

  • combat: Size a wave from the general's unit limits (9eba05c)

  • combat: Size a wave from the target owner's level (2cb8674)

  • combat: Tool bonuses are fractions, and each placed tool feeds back (ccd7023)

  • gamedata: Do not reuse a cache written against older tables (5046412)

  • map: Read an NPC camp's own fields instead of guessing them (2e786a8)

  • protocol: Stop treating any payload key E as an error code (b5ada61)

Chores

  • Keep local review notes out of the repo (63b75a2)

Code Style

  • Use American spelling throughout (70112db)

Documentation

  • Add a fill-waves example (99deb7b)

  • The example and README only need a target now (649ea60)

  • combat: Catalogue every effect that can change a wave (5dcf736)

  • combat: Close the relic map encoding question (0b41e99)

  • combat: Record which level drives which quantity (ea42a90)

  • combat: Say why the unit buff checks no conditions (8d52109)

  • combat: State which way the tool discard quirk was decided (8dfa4e4)

Features

  • attack: Build a complete attack in one call, and let tools into the pool (9614a0a)

  • attack: Count the Hall of Legends skills in flank sizing (8ea1045)

  • attack: Derive a camp's level from its victory count (5398d94)

  • attack: Fill waves from a castle's inventory (f6995a8)

  • attack: Fill waves on samurai and daimyo targets (51d1cf1)

  • attack: Read a spied castle's defenders per flank (d65d5f8)

  • attack: Read the area type off the target row (fbcfd5e)

  • attack: Read the attack pre-calculation (21cbda7)

  • attack: Read the defending castellan's effects (69c251b)

  • attack: Read the generals and skills that size a wave (ac3f8f9)

  • combat: Buff unit attack values from active global effects (17086b7)

  • combat: Count a tool's effects, not just its columns (999a182)

  • combat: Fill a wave's units the way the client's auto-fill does (651a07b)

  • combat: Fill tools and the courtyard wave into an attack (bb2b16e)

  • combat: Gate tools by target, and send every yard slot (f6cbef3)

  • combat: Give camps their walls, and fix a scaling round-trip (8cc4387)

  • combat: Pick tools with the client's five strategies (11c2efa)

  • combat: Place tools in a flank's slots (cbbb547)

  • combat: Port the flank attack and defence maths (8615432)

  • combat: Read fortification from the event camp tables (fbf61c3)

  • combat: Resolve commander bonuses, in the right id space (15c366c)

  • combat: Resolve the bonus sources that are not equipment (3ec7163)

  • combat: Score units with the commander's attack multipliers (75c21a8)

  • combat: Size a wave from the level instead of asking the caller (644cf54)

  • combat: Size the courtyard wave (8439bd4)

  • combat: Take the live strength of a global effect, and its level bracket (d7dc1ff)

  • commanders: Parse the general assigned to a commander (3dcafdf)

  • gamedata: Load unit and tool stats from the items payload (fe794d6)

  • gamedata: Parse the combat and camp-defence tables (cbc32d9)

  • map: Read the structures that defend a location (6574428)

Refactoring

  • attack: Give fill_attack a target, not the target's data (0b4849b)

  • attack: Read the inventory once for both passes (1a0a23a)

  • attack: Simplify the fill path (275662b)

Testing

  • Type-check clean under the CI configuration (7ece9a0)

  • attack: Pin the castellan to the defence it builds (aff6982)

  • attack: Pin the courtyard wave to the RW field (37028ac)

  • attack: Stop the courtyard assertion passing on an empty wave (b50f942)

  • combat: Assert a roleless unit yields no stack value (1eec625)

  • combat: Assert the solver's invariants over random inputs (e352008)

  • combat: Pin that an out-reduced defence needs no tool (033eb9c)

v0.33.0 (2026-08-25)

Bug Fixes

  • army: Read the unit inventory the shape a live server sends (8839447)

  • attack: Require a commander instead of defaulting LID to 0 (748b277)

  • commanders: Parse the equipment graphic field as an int (116d373)

  • commanders,attack: Stop accessors raising on drifted payloads (45bc153)

  • map: Correct the map item type table and report the ruin flag (c3993eb)

Documentation

  • protocol: Re-extract the client bundle and correct the cra fields (0fc26df)

  • protocol: Record the client's map object types (3ad804d)

  • readme: Document the commander and attack APIs with an example (59bd5bb)

Features

  • attack: Confirm cra against a live server and parse its response (86a3a57)

  • attack: Send attacks with waves and a chosen commander (00bad10)

  • commanders: Rename lords to commanders and parse the full entry (97f41fa)

Breaking Changes

  • attack: AttackService.send_attack now takes commander_id as a required argument, positioned after waves, and empire_core.services.attack.NO_COMMANDER is removed.

  • commanders: Client.lords is now client.commanders, LordsService is CommandersService, get_lords() is get_commanders(), the Lord model is Commander with lord_id renamed to commander_id, and the lord_id keyword of CastleService.send_support is now commander_id.

v0.32.1 (2026-08-19)

Bug Fixes

  • spy: Trade accuracy for risk instead of refusing the mission (ac7b892)

Code Style

  • tests: Apply ruff format to the spy report fixtures (c732d1d)

v0.32.0 (2026-08-18)

Features

  • spy: Forward a spy report to other players (d3ef597)

  • spy: Parse the spied castle's fortifications (c81be4c)

  • spy: Split a spy report's army by the position it holds (0532dfb)

Testing

  • spy: Narrow SpyPlan before reading it (7431b5e)

v0.31.0 (2026-08-18)

Bug Fixes

  • network: Drop a session the server has stopped answering (8d8cce1)

  • protocol: Wait for jaa when selecting a castle (3d563d3)

  • spy: Read the mission outcome instead of assuming it succeeded (2a9df22)

Features

  • spy: Size a mission from the game's own risk formula (26528c9)

Refactoring

  • spy: Treat max risk as a send gate, not a target (e10cbd3)

v0.30.3 (2026-08-12)

Bug Fixes

  • client: Pass include_unowned_types through the scan_chunks facade (6e73d04)

v0.30.2 (2026-08-12)

Bug Fixes

  • scanner: Pass include_unowned_types through scan_chunks (1ded741)

v0.30.1 (2026-08-11)

Bug Fixes

  • scanner: Stop reporting normal short map entries as schema drift (2523fae)

v0.30.0 (2026-08-11)

Bug Fixes

  • accounts: Stop logging plaintext passwords for invalid entries (7909709)

  • accounts,pool: Credential hygiene and honest failure reporting (4746562)

  • client: Guard handler registry, type the scan API, pace bulk requests (b028fee)

  • config,pool: Per-install id, immutable default config, scoped leases (a6ae59f)

  • connection: Apply state updates before waking request waiters (5e52a4e)

  • connection: Redact credentials from frame logs and serialize lifecycle (fdcac48)

  • connection,client: Survive malformed frames and report typed errors (c2d1e95)

  • protocol: Correct moving-flag parsing and bound hostile frames (a6b2c19)

  • protocol: Log every degradation path and stop accessors raising raw pydantic (4b93996)

  • protocol: Make packet parsing total and guard alliance arrays (bbe3ec3)

  • protocol: Stop drifted payloads crashing documented accessors (51b08ec)

  • release: Keep uv.lock in step with the released version (80f7e2c)

  • scanner: Report server errors and abandoned chunks as failures (1f92e45)

  • scanner: Surface dropped map entries instead of debug-logging them (005c7e0)

  • services: Typed errors for drifted payloads and a chat unsubscribe (47fc2f3)

  • state: Atomic player and castle merges, locked callback registration (f8e4989)

  • state: Locked player snapshots and honest movement lifecycle (dded0a5)

  • state: Thread-safe movement reads and atomic castle updates (f6487a0)

  • utils: Stop reporting CDN failures as empty results (51f512a)

Chores

  • Refresh uv.lock after the version bump (cb7c67c)

Documentation

  • contributing: Stop teaching the failure modes this review had to fix (00fad2e)

  • readme: Document the state, pool and error APIs, and tidy the rest (269143d)

  • state: Document the snapshot accessors and freshness rules (6826cde)

Features

  • api: Export the pool and troop helpers, fix the docs that trap users (82c8a23)

  • api: Re-export the de-facto public surface from the package root (c1d76f5)

  • packaging: Ship py.typed and make storage deps optional (0afae77)

  • state: Pass movements to arrival callbacks and expose freshness (41b2c68)

Testing

  • Cover the services layer, login handshake and drifted payloads (ad10342)

  • Keep the type checker happy about deliberately malformed fixtures (5b7c618)

Breaking Changes

  • accounts,pool: Lease() raising instead of returning None, and .env no longer being read at import.

  • connection,client: Connect() raises NetworkError instead of leaking websocket-client/socket exceptions, and send(wait=True)/request() raise PacketError instead of pydantic ValidationError.

  • packaging: Sqlmodel and aiosqlite moved to the optional [storage] extra; importing empire_core.storage without it raises an ImportError.

  • protocol: Get_moving_flags() keys results by player id (was castle id) and only includes castles that are actually relocating.

  • utils: Get_active_events() and get_troop_ids() raise NetworkError on CDN failure instead of returning an empty list/set.

v0.29.0 (2026-08-11)

Features

  • Add include_unowned option to scan_kingdom (15cdb2e)

v0.28.0 (2026-07-07)

Bug Fixes

  • Harden protocol model registry and parsing robustness (cb5f64c)

  • Remove committed credentials, declare requests dep, gate releases on tests (924f8a2)

  • Satisfy mypy in test files and Account.alias default (d22947b)

  • State manager thread-safety and movement lifecycle (d968681)

  • Typed error propagation and race-free request/response correlation (0c1440a)

Code Style

  • Apply ruff-format to map scanner tests (2aa01bb)

Documentation

  • Align README and design docs with the shipped implementation (db96fb9)

  • Document map scanning, chunk pacing, and cheap re-scans in README (01b3b88)

Features

  • Add scan_chunks + content_chunks for cheap targeted re-scans (a490634)

  • Pace kingdom-scan requests with a configurable chunk_delay (449ed1d)

Refactoring

  • Collapse service boilerplate onto typed request/execute helpers (898d193)

  • Drop unused config constants (df12625)

  • Hoist unnecessary inline imports to module scope (e22c0e1)

  • Prune dead modules, fix pool/accounts lifecycle, add test suite (843d835)

Breaking Changes

  • Client.send(wait=True) and query methods now raise typed exceptions instead of returning None on failure.

v0.27.0 (2026-05-29)

Chores

  • Update GitHub Actions to Node.js 24-compatible versions (daf11af)

Features

  • Expand gdi model with full O fields and typed castle parsing (d8ecd75)

v0.26.0 (2026-05-26)

Bug Fixes

  • Apply ruff formatting and remove unused import in events integration (87b0c08)

Features

  • Add GameEvent dataclass and get_active_events CDN resolver (5fccf23)

  • Add get_active_events to EmpireClient and export GameEvent (bf8183f)

Refactoring

  • Use direct import for GameEvent, drop TYPE_CHECKING guard (d80b064)

Testing

  • Add smoke tests to fix CI test collection failure (c439f01)

v0.25.7 (2026-05-08)

Bug Fixes

  • Remove irrelevant tests (700ad19)

v0.25.6 (2026-05-07)

Bug Fixes

  • Prevent socket disconnects via dual-layer sliding window rate limiter (6c4218e)

Chores

  • Remove inline imports and unused files (efebab6)

v0.25.5 (2026-05-07)

Bug Fixes

  • Improve map scanner resilience and fix waiter race condition (f843e77)

v0.25.4 (2026-05-07)

Bug Fixes

  • Serialize WebSocket sends with a threading.Lock (3f22a8c)

v0.25.3 (2026-04-26)

Bug Fixes

  • Remove duplicate Army class from world_models (6085bf7)

Chores

  • Delete stale TODO.md (103c2b8)

  • Remove archive directories with dead code (c6416f4)

Documentation

  • Remove outdated pygge comparison files (e9dd4dd)

Refactoring

  • Improve EmpireClient type safety and fix alliance chat encoding (c88957d)

  • Modernize typing imports to built-in syntax (5c8d5cc)

  • Remove unnecessary TYPE_CHECKING guards in services (6a98545)

v0.25.2 (2026-04-26)

Bug Fixes

  • Correct GetTargetInfoRequest payload for adi command (#22, 02f9b88)

v0.25.1 (2026-04-06)

Bug Fixes

  • Include username in disconnect and login failure logs (#21, 214aa34)

Performance Improvements

  • Clear arrived movement IDs on each gam update to prevent unbounded growth (#21, 214aa34)

v0.25.0 (2026-03-23)

Bug Fixes

  • Pass XT packet error code to payload before parsing (18acccc)

  • Race condition in wait_for dropping packets (297ddb8)

  • Rename global_rank to is_in_ruins for R flag in AllianceMember (7777116)

Features

  • Upgrade map scanner to return player objects and fix property indices (6a06bdf)

v0.24.2 (2026-03-22)

Bug Fixes

  • Include target info in bsd response (d6840af)

  • MapItemType.EXTERNAL_KINGDOM for other kingdoms main castles (20c165c)

v0.24.1 (2026-03-22)

Bug Fixes

  • Exclude non-error commands from server error logging (9682453)

v0.24.0 (2026-03-20)

Bug Fixes

  • Remove TYPE_CHECKING from alliance.py (50de2aa)

Features

  • Add spy protocol models and service (1d7aa19)

v0.23.1 (2026-03-15)

Chores

  • Delete _archive/ dead code directory (2e250bf)

Refactoring

  • Extract scan_kingdom into MapScanner class (#1, #6, #10, #12) (50c7db6)

  • State/manager.py — dispatch dict, split _handle_gbd, merge atv/ata, multi-listener callbacks (#2, #3, #8, #9) (dd6e920)

  • Typed returns, queue-based bulk fetch, remove duplicate encoding (#4, #5, #7) (7a776ab)

v0.23.0 (2026-03-08)

Bug Fixes

  • Handle offset=1 entry format in RankingEntry parser (3990cba)

  • Support dict format in RankingEntry for llsp/llsw responses (d73ce87)

Features

  • Add unranked classmethod to RankingEntry (6ac4563)

  • Track active event IDs from sei packet and fix timezone-aware bird expiry (2ce9ffd)

v0.22.1 (2026-02-17)

Bug Fixes

  • Return timezone-aware UTC datetime from bird_end_time property (1564b15)

v0.22.0 (2026-02-08)

Bug Fixes

  • Handle target_type in movement parsing and update dependencies (a496836)

  • Improve network error logging and connection stability (4e2c0b4)

Features

  • Add player search request and response models (9f80313)

  • Add ranking and highscore protocol models (b987d11)

  • Implement player search in EmpireClient (916e131)

  • Implement ranking service (a55eb99)

v0.21.0 (2026-02-01)

Features

  • Add GGEError enum with all protocol error codes (f5034cc)

v0.20.0 (2026-01-30)

Bug Fixes

  • Include kingdom_id in castle selection to support outer kingdoms (3c049b7)

  • Propagate error_code from Packet to response models (82c7e1b)

Features

  • Add inventory service and parsing logic (0d69142)

  • Add VIP time tracking and fix is_premium to check active VIP (ed8ee85)

  • Complete SCEItem enum with full inventory mapping (f7b8b2f)

  • Implement global inventory (sce) tracking and enums (f2aea73)

Refactoring

  • Remove dcl inventory parsing, rely on global sce state (42a6aa4)

v0.19.2 (2026-01-27)

Bug Fixes

  • Network stability, map scanning retries, and protocol model updates (32e7bff)

Chores

  • Remove references to other implementations (225424f)

  • Remove temporary documentation files (0f04a18)

v0.19.1 (2026-01-23)

Bug Fixes

  • Reduce keepalive interval to 30s to prevent disconnects (9b99ec1)

v0.19.0 (2026-01-23)

Features

  • Add get_player_details_bulk for fast player lookup (cac62dd)

v0.18.0 (2026-01-23)

Bug Fixes

  • Align send_support fields with pygge patterns (add KID, rename LID) (cc1a57f)

  • Export AllianceBookmark in models init (b9cdbfe)

  • Update login sequence to match pygge (remove vck, add roundTrip, update CONM) (be1e0af)

  • Use custom TimeoutError in Connection to allow catching in Client (f3c37a6)

Features

  • Add AllianceBookmark models and service method (b9cdbfe)

  • Add Birding capabilities (Army, Lords, Alliance Search) (0cbcac9)

  • Enhance send_support with full protocol parameters from pygge (cc1a57f)

v0.17.1 (2026-01-11)

Bug Fixes

  • Change library logging from INFO to DEBUG (b31a461)

v0.17.0 (2026-01-11)

Features

  • Add alliance search (hgh command) (9629811)

v0.16.0 (2026-01-11)

Bug Fixes

  • Revert version for semantic-release (8e8e35f)

  • Rewrite scan_kingdom with sequential request/response (37fc924)

  • Wait for gbd packet after login to populate player state (3057b5e)

Features

  • Add Kingdom enum and alliance tracking support (0b6a4d4)

  • Add scan_kingdom with BFS wave expansion (d14af84)

v0.15.0 (2026-01-11)

Features

  • Expose raw commander data in movements (f461d49)

v0.14.0 (2026-01-10)

Features

  • Dispatch on_incoming_attack callback for movement updates (db39eda)

v0.13.0 (2026-01-10)

Features

  • Add activity_tier property to AllianceMember for tiered offline status (cd67fae)

Breaking Changes

  • _online property replaced with _activity_tier

v0.12.0 (2026-01-10)

Bug Fixes

  • Use AMI array for online status instead of H field (623fc10)

Breaking Changes

  • The H field was incorrectly assumed to be 'hours since online'. It's actually 'honor' points. Online status now correctly comes from the AMI array (index 4), where 0 = online and non-zero = offline.

v0.11.0 (2026-01-08)

Features

  • Add no_cache option to get_member() for fresh data (ba26735)

v0.10.1 (2026-01-08)

Refactoring

  • Rename get_my_* to get_local_* for consistency (75caebd)

v0.10.0 (2026-01-08)

Features

  • Add convenience methods for local player's alliance data (0de1e9a)

v0.9.0 (2026-01-08)

Features

  • Add alliance info command (ain) with member online status (fcc9977)

v0.8.0 (2026-01-06)

Features

  • Add on_movement_arrived callback and change recall/arrived to pass MID only (6e8084d)

v0.7.3 (2026-01-06)

Bug Fixes

  • Don't remove movements in gam handler, wait for arrival/recall packets (0b5d49f)

v0.7.2 (2026-01-06)

Bug Fixes

  • Use mrm packet for recall detection, not maa (6c8c461)

v0.7.1 (2026-01-06)

Bug Fixes

  • Detect recalls via maa packet instead of gam comparison (82ed870)

v0.7.0 (2026-01-06)

Features

  • Add estimated_size field to Movement for non-visible armies (23fed4d)

v0.6.6 (2026-01-06)

Bug Fixes

  • Handle GS as int and SA as int in gam/gal packets (51855ae)

v0.6.5 (2026-01-06)

Bug Fixes

  • Coerce SA field to string in Alliance model (b6b23aa)

Chores

  • Add info-level logging for SDI debugging (faf9564)

v0.6.4 (2026-01-06)

Bug Fixes

  • Handle lli packet for alliance info + add debug logging (7cdb545)

v0.6.3 (2026-01-06)

Bug Fixes

  • Dispatch callbacks in thread pool to avoid blocking receive loop (a30562f)

v0.6.2 (2026-01-06)

Bug Fixes

  • Get_max_defense returns yard_limit only (includes support capacity) (22c9fdc)

v0.6.1 (2026-01-06)

Bug Fixes

  • Correct castle coordinate parsing from lli response (db057d1)

v0.6.0 (2026-01-05)

Features

  • Add defense capacity limits (yard_limit, wall_limit) to SDI response (ec886df)

v0.5.0 (2026-01-05)

Features

  • Add SDI (Support Defense Info) command for querying alliance castle defense (abcc58d)

v0.4.5 (2026-01-05)

Bug Fixes

  • Extract GA units from wrapper level, not inside UM (d645968)

v0.4.4 (2026-01-05)

Bug Fixes

  • Parse GA (Garrison Army) units from movement wrapper (83ff404)

Chores

v0.4.3 (2026-01-04)

Bug Fixes

  • Route all packets through GameState for callbacks (88ada1b)

Chores

v0.4.2 (2026-01-05)

Bug Fixes

  • Include T=0 as attack movement type (1b4b219)

v0.4.1 (2026-01-05)

Bug Fixes

  • Trigger attack callback for all attacks, not just incoming (0592812)

v0.4.0 (2026-01-04)

Bug Fixes

  • Use dynamic version from package metadata (c09a706)

Features

  • Add troop filtering, alliance names, and recall detection (803ac07)

v0.3.1 (2026-01-04)

Bug Fixes

  • Use dynamic version from package metadata (#4, 4efe3d5)

v0.3.0 (2026-01-04)

Bug Fixes

  • cd: Pass built artifacts from release job to publish job (#3, 615483d)

  • cd: Trigger release on push to master instead of CI workflow_run (#3, 615483d)

  • cd: Use no-commit mode for semantic-release to work with branch protection (#3, 615483d)

  • cd: Use RELEASE_TOKEN PAT for semantic-release (#3, 615483d)

  • ci: Align job names with branch protection rules (#2, 0957f15)

  • ci: Only run CI on pull requests (#3, 615483d)

  • ci: Only run CI on pull requests, not on merge to master (#3, 615483d)

Chores

  • Remove stale documentation and empty test (#2, 0957f15)

Features

  • Add protocol models and service layer (#2, 0957f15)

  • Add service layer with auto-registration (#2, 0957f15)

  • protocol: Add Pydantic models for GGE protocol commands (#2, 0957f15)

Performance Improvements

  • Optimize packet dispatch for high message volume (#2, 0957f15)

v0.2.1 (2026-01-04)

Bug Fixes

  • Ensure publish job gets latest version after semantic-release bump (272e3c3)

  • Exclude _archive from pytest collection (2ba8b8c)

  • Require env vars for credentials in examples (cf005e3)

  • Resolve CI failures by archiving old async code and fixing type errors (a514161)

Code Style

  • Auto-fix ruff lint errors (3483e3e)

Refactoring

  • Replace async architecture with sync + threading (67315c6)

v0.2.0 (2025-12-31)

Features

  • Add send_support and get_bookmarks actions for troop birding (5a14466)

v0.1.0 (2025-12-29)

  • Initial Release