PST Pal Editor Engine (pal_editor/ subpackage)
Originally a 5454-line monolith (edit_pals.py), now a modular package at src/palworld_aio/editor/pal_editor/ with 17 modules + backward-compat shim (edit_pals.py re-exports all public symbols).
Module structure (17 modules)
| Module |
Key contents |
__init__.py |
Re-exports all public symbols |
data.py |
Data loaders, caches, constants (_ensure_passive_data, _ensure_skill_data) |
icons.py |
Icons, pixmaps, badges, _strip_prefix_label |
pal_ops.py |
Raw-dict mutation functions, build_pal_context_menu |
widgets.py |
Self-contained widget classes (StrokedLabel, _ShinyStar, etc.) |
card_widgets.py |
Pal card grid rendering |
party_slot_widget.py |
Party slot (5 slots) |
palbox_slot_widget.py |
Palbox slot (30 per box) |
pal_info_widget.py |
PalInfoWidget — main detailed editor panel |
pal_info_display.py |
PalInfoWidget display mixin (render updates, element colors) |
pal_info_handlers.py |
PalInfoWidget handler mixin (click handlers for all editable fields) |
create_dialogs.py |
BulkSyncPalDialog, PalCreateDialog, _show_learned_moves_dialog |
pal_editor_widget.py |
PalEditorWidget — main composition widget |
pal_editor_bulk_ops.py |
BulkOperationMixin (batch rename/feed/heal) |
pal_editor_global_ops.py |
Global ops (delete/remove-skill from all pals) |
legacy_frame.py |
PalFrame — legacy frame with global game-data maps |
edit_pals.py (shim) |
Backward-compat re-exports |
Classes (23)
Key ones:
- PalEditorWidget (pal_editor_widget.py, QWidget) — main composition: partyPanel (5×PartySlotWidget) + palboxPanel (5×6 grid of 30 PalboxSlotWidget, 32 boxes) + PalInfoWidget. Hotkeys Q/E=box nav, F=focus info.
- PalInfoWidget (pal_info_widget.py, QFrame) — THE detailed editor panel. Signal pal_data_changed. All click-edit handlers. Mixin: pal_info_display.py + pal_info_handlers.py.
- PalIcon (widgets.py) / PalCardWidget (card_widgets.py) — grid cells with badges (boss/lucky/awake/dna/fav).
- PartySlotWidget (party_slot_widget.py) / PalboxSlotWidget (palbox_slot_widget.py) — slot widgets. Signals: clicked, rightClicked(slot,action), entered, left. Double-click→delete/add.
- PalFrame (legacy_frame.py) — legacy display frame; holds global game-data maps (_NAMEMAP, _PASSMAP, _SKILLMAP, _PASSRANK, _PASSFLAGS) via _load_maps.
- PalCreateDialog (create_dialogs.py) — searchable species picker → _generate_pal_save_param → append to save+container+guild.
- BulkSyncPalDialog (create_dialogs.py) — copy _EDITABLE_KEYS from one pal to same-species pals.
Known extraction-regression bugs (⚠️ pitfall warning)
During the Phase 1 partition (monolith → subpackage), 7 out of ~10 extracted methods were broken during extraction into pal_info_handlers.py. These are not edge cases — they are systemic extraction errors to guard against:
_on_soul_click: Missing dlg.exec() + write-back + _recalc_hp() calls. The dialog was constructed but never shown, and user input was never applied.
_show_skill_picker: Rewritten as a stub — passed wrong args, used wrong method name (exec() vs pick()), referenced non-existent selected_asset.
_tick_star_shine: Rewritten to call .flash() on one label (method doesn't exist). Original advances phase counter incrementally and calls set_phase() on ALL shine labels.
_set_active_skill: Wrong array_type (NameProperty → needs EnumProperty), wrong item format (dict vs plain string), missing empty-item filtering, missing EPalWazaID:: prefix on values.
_get_current_passive_list: Missing dict-unwrapping (v['value'] when items are dicts).
- Caller of skill-picker: Passed
_data._SKILL_DATA (dict-of-dicts) instead of PalFrame._SKILLMAP (flat asset→name dict).
_on_portrait_context_menu (pal_info_widget.py): build_pal_context_menu was rewritten to return a single ScrollableContextMenu (string-key dispatch), but this caller was left on the old menu, actions = ... tuple-unpacking pattern inherited from the monolith.
Guard against extraction regressions by:
- Comparing EVERY line of extracted methods to the original (never rewrite from scratch).
- Auditing ALL callers when a shared utility's return signature changes.
- Running
diff <(git show HEAD:original_file) extracted_file on extracted sections.
- Loading the QA audit protocol from project memory (§8 Errors and fixes) for any future extraction work.
Pal data model (THE save structure)
Access path: entry['value']['RawData']['value']['object']['SaveParameter']['value'] → flat dict of {field: typed_wrapper}.
Type-wrapper shapes:
- ByteProperty:
{id,type:'ByteProperty',value:{type:'None',value:<int>}}
- IntProperty/Int64Property:
{id,type,value:<int>}
- FloatProperty:
{id,type,value:<float>}
- BoolProperty:
{id,type,value:<bool>}
- NameProperty/StrProperty:
{id,type,value:<str>}
- EnumProperty:
{id,type,value:{type:<EnumType>,value:<EnumStr>}}
- StructProperty(FixedPoint64): HP stored ×1000:
{struct_type:'FixedPoint64',value:{Value:{value:<int>}}}
- StructProperty(Guid):
{struct_type:'Guid',value:<uuid-str>}
- ArrayProperty(Name/Enum):
{array_type,value:{values:[<str>...]}}
Editable fields + legal ranges:
| Field |
Wrapper |
Range |
Notes |
| CharacterID |
NameProperty |
valid asset |
BOSS_ prefix = boss |
| Level |
Byte/Int |
1-80 |
illegal if >80 |
| Exp |
Int64 |
from pal_exp_table |
_set_level sets both |
| Gender |
Enum EPalGenderType |
::Male/::Female |
toggle |
| NickName |
StrProperty |
str |
|
| Hp/MaxHP |
Struct FixedPoint64 (×1000) |
|
recalced via calculate_max_hp |
| Talent_HP/Talent_Shot/Talent_Defense |
Byte |
0-100 (IVs) |
illegal if >100 |
| Rank_HP/Rank_Attack/Rank_Defence/Rank_CraftSpeed |
Byte |
0-20 (souls) |
illegal if >20 |
| Rank |
Byte |
1-5 (condenser stars) |
illegal if >5 |
| FriendshipPoint |
Int |
0-200000 |
trust; thresholds from friendship.json |
| PassiveSkillList |
Array(Name) |
≤4 |
illegal if >4; _set_passive_skill caps [:4] |
| EquipWaza |
Array(Enum EPalWazaID::) |
≤3 non-empty |
illegal if >3; _set_active_skill caps [:3] |
| MasteredWaza |
Array(Enum) |
unbounded |
learnable; _learn_all_skills fills |
| IsRarePal |
Bool |
|
"lucky"/shiny |
| bIsAwakening |
Bool |
|
awakened (×1.028 HP) |
| bImportedCharacter |
Bool |
|
"DNA/imported" flag |
| FavoriteIndex |
Byte |
0-3 |
lock level |
| FullStomach |
Float |
~0-300 |
hunger |
| SanityValue |
Float |
0-100 |
SAN |
| SlotId |
Struct PalCharacterSlotId |
|
{ContainerId:{ID:{Guid}}, SlotIndex:{Int}} |
| OwnerPlayerUId |
Struct(Guid) |
|
owner |
Stat formulas (utils.py):
- calculate_max_hp(205):
floor(500 + 5·lvl + hp_scaling·0.5·lvl·(1+IV·0.3/100)·α)·(1+condenser_bonus)·(1+soul·0.03)·(1+trust·0.03)·awaken(1.028) ×1000; α=1.087 if boss/lucky; condenser_bonus=(Rank-1)·0.05.
- calculate_shot_attack(218) / calculate_defense(239) / calculate_attack(229): similar scaling.
Pal generation
_generate_pal_save_param(character_id, nickname, owner_uid, container_id, slot_index, group_id=None) (1808): builds full cmap entry. Defaults: Gender=Female, Talent_*=100 (maxed IVs), FullStomach=150, Hp=calculate_max_hp(lvl1,100iv). EquipWaza=[Unique_Roll] only for SheepBall. OwnedTime=638486453957560000. NO Level/Exp/Rank/souls given (game defaults).
_register_pal_instance_to_guild(instance_id, group_id) (1731): appends {guid,instance_id} to GroupSaveDataMap matching guild's individual_character_handle_ids.
Create wiring (PalCreateDialog._on_create 5235): resolve group_id from player's guild → compute slot ((box-1)*30+slot) → append cmap entry → append CharacterContainerSaveData slot → register to guild → increment PLAYER_PAL_COUNTS.
Editing operations
All mutate self._raw in place then self._refresh(). (Line numbers reference the original edit_pals.py monolith; use git log -p -S 'method_name' to find current locations.)
| Op |
Method(line) |
Edits |
| Level |
_set_level(3543) |
Level,Exp,Hp,MaxHP |
| Stars/Rank |
_on_star_click(3457) |
Rank (cycles 1-5) |
| IVs |
_on_talent_click(3575) |
Talent_HP/Shot/Defense (0-100) |
| Souls |
_on_soul_click(3591) |
Rank_HP/Attack/Defence/CraftSpeed (0-20) |
| Gender |
_on_gender_click(3446) |
Gender enum |
| Nickname |
_on_name_click(3431) |
NickName |
| Boss toggle |
_toggle_boss_raw(1613) |
CharacterID (BOSS_ prefix), IsRarePal |
| Lucky toggle |
_toggle_lucky_raw(1623) |
IsRarePal, CharacterID |
| Awakened |
_toggle_awake_raw(1631) |
bIsAwakening |
| DNA/imported |
_toggle_dna_raw(1633) |
bImportedCharacter |
| Favorite/lock |
_set_fav_raw(1635) |
FavoriteIndex (0-3) |
| Trust |
_on_trust_click(3520) |
FriendshipPoint |
| Active skill |
_set_active_skill(3666) |
EquipWaza (caps [:3]) |
| Passive skill |
_set_passive_skill(3679) |
PassiveSkillList (caps [:4]) |
| Learn all |
_learn_all_skills_raw(1560) |
MasteredWaza(full)+EquipWaza(≤3 by power) |
| Work suitability |
_set_work_suitability(1678) |
GotWorkSuitabilityAddRankList |
| Max all |
_max_stats_raw(1713) |
IVs=100,souls=20,Rank=5,trust=200k,awake,WS=10,lvl=80 |
| Bulk sync |
BulkSyncPalDialog._on_apply(4327) |
copies _EDITABLE_KEYS to same-species |
NO in-place species swap — species change only via BOSS_ toggle or delete+create.
NO move-between-containers — transfer = delete+create. All DnD explicitly disabled (setAcceptDrops(False)).
EDITABLE_KEYS(4195): Level,Exp,Gender,Talent,Rank_,Rank,FriendshipPoint,IsRarePal,bIsAwakening,bImportedCharacter,FavoriteIndex,EquipWaza,MasteredWaza,PassiveSkillList,Hp,MaxHP,GotWorkSuitabilityAddRankList.
Illegal pal detection (func_manager.py)
check_is_illegal_pal(1637) → (is_illegal, markers). Thresholds: Level>80, any IV>100, any Soul>20, Passives>4, ActiveSkills>3(non-empty), Rank>5.
fix_illegal_pals_in_save(1857): clamps to ceilings (Level→80+Exp, IVs→100, souls→20, Rank→5, passives→[:4], active→[:3]). Processes CharacterSaveParameterMap + per-player *_dps.sav via ProcessPoolExecutor.
remove_invalid_pals_from_save(881): drops cmap entries whose CharacterID ∉ characters.json pals∪npcs.
remove_invalid_passives_from_save(1248): removes PassiveSkillList entries ∉ skills.json passives.
Container/slot model
- Party container = player .sav SaveData.OtomoCharacterContainerId.ID; palbox = PalStorageContainerId.ID. DPS file = _dps.sav.
- CharacterContainerSaveData: {key:{ID:{Guid}}, value:{Slots:{values:[{SlotIndex,RawData:{instance_id}}]}}}.
- Palbox: 32 boxes × 30 slots (5×6). abs_index = (box-1)*30 + slot. Party: 5 slots.
- ContainerOwnership.build(cmap,container_data): majority-vote maps instance→container→owner. get_effective_owner(66) resolves base-worker pals.
- PLAYER_PAL_COUNTS[uid] tracks counts; incremented on create, decremented on delete.
Game data binding
PalFrame._load_maps(5300): _NAMEMAP(pals+npcs), _PASSMAP(passives), _SKILLMAP(skills), _PAL_ZUKAN(zukan_index), _PASSRANK/PASSFLAGS. All via data_manager.load_game_data_map(file,key) → {asset.lower():name}. Passives filtered: category=='SortDisplayable' AND no add_weapon/armor/accessory.
get_pal_base_data(cid)(93): lazy characters.json cache, backfills from boss/gym variants.
Passive rank colors: _RANK_COLORS(839) maps -99/1/2/4/5; ≥5=world_tree overlay, ≥4=legend.
1---2name: pst-pal-editor3description: The pal editing engine — pal data model (every SaveParameter field with type-wrapper shapes), editing operations, illegal pal detection, pal generation, container/slot model, game data binding, and the PalEditorWidget UI. Previously the monolithic `edit_pals.py` (5454 lines), now a modular `pal_editor/` package (17 modules + backward-compat `edit_pals.py` shim). Load when editing pal logic, stats, skills, IVs, containers, or illegal detection.4---56# PST Pal Editor Engine (pal_editor/ subpackage)78Originally a 5454-line monolith (`edit_pals.py`), now a modular package at `src/palworld_aio/editor/pal_editor/` with 17 modules + backward-compat shim (`edit_pals.py` re-exports all public symbols).910## Module structure (17 modules)1112| Module | Key contents |13|---|---|14| `__init__.py` | Re-exports all public symbols |15| `data.py` | Data loaders, caches, constants (`_ensure_passive_data`, `_ensure_skill_data`) |16| `icons.py` | Icons, pixmaps, badges, `_strip_prefix_label` |17| `pal_ops.py` | Raw-dict mutation functions, `build_pal_context_menu` |18| `widgets.py` | Self-contained widget classes (`StrokedLabel`, `_ShinyStar`, etc.) |19| `card_widgets.py` | Pal card grid rendering |20| `party_slot_widget.py` | Party slot (5 slots) |21| `palbox_slot_widget.py` | Palbox slot (30 per box) |22| `pal_info_widget.py` | `PalInfoWidget` — main detailed editor panel |23| `pal_info_display.py` | PalInfoWidget display mixin (render updates, element colors) |24| `pal_info_handlers.py` | PalInfoWidget handler mixin (click handlers for all editable fields) |25| `create_dialogs.py` | `BulkSyncPalDialog`, `PalCreateDialog`, `_show_learned_moves_dialog` |26| `pal_editor_widget.py` | `PalEditorWidget` — main composition widget |27| `pal_editor_bulk_ops.py` | `BulkOperationMixin` (batch rename/feed/heal) |28| `pal_editor_global_ops.py` | Global ops (delete/remove-skill from all pals) |29| `legacy_frame.py` | `PalFrame` — legacy frame with **global game-data maps** |30| `edit_pals.py` (shim) | Backward-compat re-exports |3132## Classes (23)33Key ones:34- **PalEditorWidget** (pal_editor_widget.py, QWidget) — main composition: partyPanel (5×PartySlotWidget) + palboxPanel (5×6 grid of 30 PalboxSlotWidget, 32 boxes) + PalInfoWidget. Hotkeys Q/E=box nav, F=focus info.35- **PalInfoWidget** (pal_info_widget.py, QFrame) — THE detailed editor panel. Signal pal_data_changed. All click-edit handlers. Mixin: pal_info_display.py + pal_info_handlers.py.36- **PalIcon** (widgets.py) / **PalCardWidget** (card_widgets.py) — grid cells with badges (boss/lucky/awake/dna/fav).37- **PartySlotWidget** (party_slot_widget.py) / **PalboxSlotWidget** (palbox_slot_widget.py) — slot widgets. Signals: clicked, rightClicked(slot,action), entered, left. Double-click→delete/add.38- **PalFrame** (legacy_frame.py) — legacy display frame; **holds global game-data maps** (_NAMEMAP, _PASSMAP, _SKILLMAP, _PASSRANK, _PASSFLAGS) via _load_maps.39- **PalCreateDialog** (create_dialogs.py) — searchable species picker → _generate_pal_save_param → append to save+container+guild.40- **BulkSyncPalDialog** (create_dialogs.py) — copy _EDITABLE_KEYS from one pal to same-species pals.4142## Known extraction-regression bugs (⚠️ pitfall warning)4344During the Phase 1 partition (monolith → subpackage), **7 out of ~10 extracted methods were broken** during extraction into `pal_info_handlers.py`. These are not edge cases — they are systemic extraction errors to guard against:45461. **`_on_soul_click`**: Missing `dlg.exec()` + write-back + `_recalc_hp()` calls. The dialog was constructed but never shown, and user input was never applied.472. **`_show_skill_picker`**: Rewritten as a stub — passed wrong args, used wrong method name (`exec()` vs `pick()`), referenced non-existent `selected_asset`.483. **`_tick_star_shine`**: Rewritten to call `.flash()` on one label (method doesn't exist). Original advances phase counter incrementally and calls `set_phase()` on ALL shine labels.494. **`_set_active_skill`**: Wrong `array_type` (`NameProperty` → needs `EnumProperty`), wrong item format (dict vs plain string), missing empty-item filtering, missing `EPalWazaID::` prefix on values.505. **`_get_current_passive_list`**: Missing dict-unwrapping (`v['value']` when items are dicts).516. **Caller of skill-picker**: Passed `_data._SKILL_DATA` (dict-of-dicts) instead of `PalFrame._SKILLMAP` (flat asset→name dict).527. **`_on_portrait_context_menu`** (`pal_info_widget.py`): `build_pal_context_menu` was rewritten to return a single `ScrollableContextMenu` (string-key dispatch), but this caller was left on the old `menu, actions = ...` tuple-unpacking pattern inherited from the monolith.5354**Guard against extraction regressions by:**55- Comparing EVERY line of extracted methods to the original (never rewrite from scratch).56- Auditing ALL callers when a shared utility's return signature changes.57- Running `diff <(git show HEAD:original_file) extracted_file` on extracted sections.58- Loading the QA audit protocol from project memory (§8 Errors and fixes) for any future extraction work.5960<div style="page-break-after: always;"></div>6162## Pal data model (THE save structure)63Access path: `entry['value']['RawData']['value']['object']['SaveParameter']['value']` → flat dict of `{field: typed_wrapper}`.6465**Type-wrapper shapes:**66- ByteProperty: `{id,type:'ByteProperty',value:{type:'None',value:<int>}}`67- IntProperty/Int64Property: `{id,type,value:<int>}`68- FloatProperty: `{id,type,value:<float>}`69- BoolProperty: `{id,type,value:<bool>}`70- NameProperty/StrProperty: `{id,type,value:<str>}`71- EnumProperty: `{id,type,value:{type:<EnumType>,value:<EnumStr>}}`72- StructProperty(FixedPoint64): HP stored ×1000: `{struct_type:'FixedPoint64',value:{Value:{value:<int>}}}`73- StructProperty(Guid): `{struct_type:'Guid',value:<uuid-str>}`74- ArrayProperty(Name/Enum): `{array_type,value:{values:[<str>...]}}`7576**Editable fields + legal ranges:**77| Field | Wrapper | Range | Notes |78|---|---|---|---|79| CharacterID | NameProperty | valid asset | BOSS_ prefix = boss |80| Level | Byte/Int | 1-80 | illegal if >80 |81| Exp | Int64 | from pal_exp_table | _set_level sets both |82| Gender | Enum EPalGenderType | ::Male/::Female | toggle |83| NickName | StrProperty | str | |84| Hp/MaxHP | Struct FixedPoint64 (×1000) | | recalced via calculate_max_hp |85| Talent_HP/Talent_Shot/Talent_Defense | Byte | 0-100 (IVs) | illegal if >100 |86| Rank_HP/Rank_Attack/Rank_Defence/Rank_CraftSpeed | Byte | 0-20 (souls) | illegal if >20 |87| Rank | Byte | 1-5 (condenser stars) | illegal if >5 |88| FriendshipPoint | Int | 0-200000 | trust; thresholds from friendship.json |89| PassiveSkillList | Array(Name) | ≤4 | illegal if >4; _set_passive_skill caps [:4] |90| EquipWaza | Array(Enum EPalWazaID::) | ≤3 non-empty | illegal if >3; _set_active_skill caps [:3] |91| MasteredWaza | Array(Enum) | unbounded | learnable; _learn_all_skills fills |92| IsRarePal | Bool | | "lucky"/shiny |93| bIsAwakening | Bool | | awakened (×1.028 HP) |94| bImportedCharacter | Bool | | "DNA/imported" flag |95| FavoriteIndex | Byte | 0-3 | lock level |96| FullStomach | Float | ~0-300 | hunger |97| SanityValue | Float | 0-100 | SAN |98| SlotId | Struct PalCharacterSlotId | | {ContainerId:{ID:{Guid}}, SlotIndex:{Int}} |99| OwnerPlayerUId | Struct(Guid) | | owner |100101**Stat formulas (utils.py):**102- calculate_max_hp(205): `floor(500 + 5·lvl + hp_scaling·0.5·lvl·(1+IV·0.3/100)·α)·(1+condenser_bonus)·(1+soul·0.03)·(1+trust·0.03)·awaken(1.028)` ×1000; α=1.087 if boss/lucky; condenser_bonus=(Rank-1)·0.05.103- calculate_shot_attack(218) / calculate_defense(239) / calculate_attack(229): similar scaling.104105## Pal generation106**_generate_pal_save_param(character_id, nickname, owner_uid, container_id, slot_index, group_id=None)** (1808): builds full cmap entry. Defaults: Gender=Female, Talent_*=100 (maxed IVs), FullStomach=150, Hp=calculate_max_hp(lvl1,100iv). EquipWaza=[Unique_Roll] only for SheepBall. OwnedTime=638486453957560000. NO Level/Exp/Rank/souls given (game defaults).107108**_register_pal_instance_to_guild(instance_id, group_id)** (1731): appends {guid,instance_id} to GroupSaveDataMap matching guild's individual_character_handle_ids.109110**Create wiring** (PalCreateDialog._on_create 5235): resolve group_id from player's guild → compute slot ((box-1)*30+slot) → append cmap entry → append CharacterContainerSaveData slot → register to guild → increment PLAYER_PAL_COUNTS.111112## Editing operations113All mutate self._raw in place then self._refresh(). _(Line numbers reference the original `edit_pals.py` monolith; use `git log -p -S 'method_name'` to find current locations.)_114| Op | Method(line) | Edits |115|---|---|---|116| Level | _set_level(3543) | Level,Exp,Hp,MaxHP |117| Stars/Rank | _on_star_click(3457) | Rank (cycles 1-5) |118| IVs | _on_talent_click(3575) | Talent_HP/Shot/Defense (0-100) |119| Souls | _on_soul_click(3591) | Rank_HP/Attack/Defence/CraftSpeed (0-20) |120| Gender | _on_gender_click(3446) | Gender enum |121| Nickname | _on_name_click(3431) | NickName |122| Boss toggle | _toggle_boss_raw(1613) | CharacterID (BOSS_ prefix), IsRarePal |123| Lucky toggle | _toggle_lucky_raw(1623) | IsRarePal, CharacterID |124| Awakened | _toggle_awake_raw(1631) | bIsAwakening |125| DNA/imported | _toggle_dna_raw(1633) | bImportedCharacter |126| Favorite/lock | _set_fav_raw(1635) | FavoriteIndex (0-3) |127| Trust | _on_trust_click(3520) | FriendshipPoint |128| Active skill | _set_active_skill(3666) | EquipWaza (caps [:3]) |129| Passive skill | _set_passive_skill(3679) | PassiveSkillList (caps [:4]) |130| Learn all | _learn_all_skills_raw(1560) | MasteredWaza(full)+EquipWaza(≤3 by power) |131| Work suitability | _set_work_suitability(1678) | GotWorkSuitabilityAddRankList |132| Max all | _max_stats_raw(1713) | IVs=100,souls=20,Rank=5,trust=200k,awake,WS=10,lvl=80 |133| Bulk sync | BulkSyncPalDialog._on_apply(4327) | copies _EDITABLE_KEYS to same-species |134135**NO in-place species swap** — species change only via BOSS_ toggle or delete+create.136**NO move-between-containers** — transfer = delete+create. All DnD explicitly disabled (setAcceptDrops(False)).137138_EDITABLE_KEYS(4195): Level,Exp,Gender,Talent_*,Rank_*,Rank,FriendshipPoint,IsRarePal,bIsAwakening,bImportedCharacter,FavoriteIndex,EquipWaza,MasteredWaza,PassiveSkillList,Hp,MaxHP,GotWorkSuitabilityAddRankList.139140## Illegal pal detection (func_manager.py)141**check_is_illegal_pal(1637)** → (is_illegal, markers). Thresholds: Level>80, any IV>100, any Soul>20, Passives>4, ActiveSkills>3(non-empty), Rank>5.142**fix_illegal_pals_in_save(1857)**: clamps to ceilings (Level→80+Exp, IVs→100, souls→20, Rank→5, passives→[:4], active→[:3]). Processes CharacterSaveParameterMap + per-player *_dps.sav via ProcessPoolExecutor.143**remove_invalid_pals_from_save(881)**: drops cmap entries whose CharacterID ∉ characters.json pals∪npcs.144**remove_invalid_passives_from_save(1248)**: removes PassiveSkillList entries ∉ skills.json passives.145146## Container/slot model147- Party container = player .sav SaveData.OtomoCharacterContainerId.ID; palbox = PalStorageContainerId.ID. DPS file = <uid>_dps.sav.148- CharacterContainerSaveData: {key:{ID:{Guid}}, value:{Slots:{values:[{SlotIndex,RawData:{instance_id}}]}}}.149- Palbox: 32 boxes × 30 slots (5×6). abs_index = (box-1)*30 + slot. Party: 5 slots.150- ContainerOwnership.build(cmap,container_data): majority-vote maps instance→container→owner. get_effective_owner(66) resolves base-worker pals.151- PLAYER_PAL_COUNTS[uid] tracks counts; incremented on create, decremented on delete.152153## Game data binding154PalFrame._load_maps(5300): _NAMEMAP(pals+npcs), _PASSMAP(passives), _SKILLMAP(skills), _PAL_ZUKAN(zukan_index), _PASSRANK/_PASSFLAGS. All via data_manager.load_game_data_map(file,key) → {asset.lower():name}. Passives filtered: category=='SortDisplayable' AND no add_weapon/armor/accessory.155get_pal_base_data(cid)(93): lazy characters.json cache, backfills from boss_/gym variants.156Passive rank colors: _RANK_COLORS(839) maps -99/1/2/4/5; ≥5=world_tree overlay, ≥4=legend.