-
Notifications
You must be signed in to change notification settings - Fork 0
App Core
PCEdit.App.Core is everything that is the app but not a UI framework: ViewModels, domain
services, the item catalogs, localization, and the interfaces a UI head must implement.
It has no reference to Avalonia. That boundary is what makes the app layer testable headlessly —
PCEdit.App.Core.Tests drives real ViewModels with fake platform services.
Services/ISaveFileWorkspace (SaveFileWorkspace) is a DI singleton holding the loaded
PlanetCrafterSaveFile, its path, IsDirty and SaveStatus.
PlanetCrafterSaveFile? Current { get; }
string? FilePath { get; }
bool IsLoaded { get; }
bool IsDirty { get; }
string? SaveStatus { get; }
void Load(string path);
void Save();
void MutateUnlocks(Func<SaveFileUnlocks, SaveFileUnlocks> mutate);
void ReplaceTerraformation(string planetId, Func<PlanetTerraformation, PlanetTerraformation> mutate);
void ReplacePlayer(long playerId, Func<PlayerData, PlayerData> mutate);
void ReplaceInventory(int inventoryId, Func<Inventory, Inventory> mutate);
int GrantTerraTokens(long playerId, int amount);ViewModels never build a modified save themselves. They call one of these, which applies the
root-rebuild-vs-list-replace pattern from Save File Library and flips IsDirty.
Centralising it is the reason an edit cannot accidentally drop a field it did not know about.
GrantTerraTokens is the one composite operation: it raises the save's balance, the all-time total,
and the chosen player's earned count, because a balance above the all-time total is a state the
game never produces. All three are 32-bit and saturate at the maximum rather than wrapping to a
negative balance; the return value is what the balance could actually take, so a clamped grant can
be reported honestly.
Save also copies the file aside first, through ISaveBackupService — once per file opened rather
than once per save, since by the second save the file on disk is already PCEdit's own output and the
pristine copy is the only one that cannot be reconstructed. A backup that fails is logged and the
save proceeds: refusing the user's edit because a safety copy failed inverts the priority.
| Service | What it owns |
|---|---|
IInventoryEditor / InventoryEditor
|
Inventory grouping, item moves, logistics config |
ContainerOrigins |
Who placed a container — Map (ids 100,000,000–199,999,999, the developers' objects), Wreck (wreck-only types, or listed in a procedural instance's woIdsGenerated), Built (a runtime id of a type never found in wrecks), else Unknown
|
ItemStackKey |
What makes two items "identical" for a stack row: everything but their id and where they lay (pos / rot / planet) |
IOverflowRepair / OverflowRepair
|
The Repair: plan and apply trimming over-full inventories to N × size (never past 7,200), after moving ticked types into free storage |
FreeStorage |
Free slots for the Repair: Built Container1/2/3 only, not logistics, same planet, a crate already holding the type first |
SaveCopyName |
The suggested file name for a repaired copy — the next free <Mode>-<N>.json slot, else a -repaired suffix |
IPlanetIndex / PlanetIndex
|
Which worlds exist, and which world a WorldObject.Planet hash is |
IItemCatalog / ItemCatalog
|
GId → display name, category icon, logistics flags, game-version metadata |
ILogisticsGroupCatalog / LogisticsGroupCatalog
|
The logistics pick-lists — a projection of the item catalog's canDemand / canSupply flags, minus deprecated items (no data file of its own since v1.4.0) |
IdSearch |
The "search by id" rule shared by the Inventories page and the Move dialog: a digits-only term (optional #) matches ids by prefix and does not fall back to text |
WorldObjectIdsCodec |
The inventory woIds comma-string — skips entries it cannot read, and carries them through a rewrite untouched |
GroupListCodec |
The demandGrps / supplyGrps comma-strings |
PositionCodec |
The "x,y,z" string shared by player and object positions — use TryParse for anything read out of a save |
-
BuildInventoryGroups()is O(n). It pre-indexes world objects and container → inventory links once; a real multi-planet save has ~1,000 inventories and ~12,000 world objects, and the naive nested scan is visibly slow. EachInventoryGroupis tagged with anInventoryKind(for the page's type filter), aPlanetIdresolved viaIPlanetIndex(for its world filter) and aContainerOrigin. A machine's second (output) inventory is linked throughsiIds, notliId; both are indexed,liIdwinning. -
TryMoveItems(withTryMoveItemas its one-item case) moves items all or none: it changes nothing unless they are all in one source and the destination has room for every one against its storedInventory.Size, so PCEdit never makes an inventory over-full. Moving out of an over-full one is always allowed. -
GetDestinationOptionsbuilds the Move dialog's list. EachInventoryOptionViewcarries itsKindand owning container's object id, and has aMatches()usingIdSearch, so the dialog and the page search the same way. -
GetLogisticsContainer/UpdateLogisticsread and write demand groups, supply groups and priority. Only logistics containers carry aprioritykey, which is how one is recognised.
InventoryGroup (one Inventories card) is a record since v1.5.0, so a filtered copy is a with
expression that cannot drop a property. NarrowTo(query) returns the card to show for a search:
- the card itself when the inventory matches (label, world, inventory id, container object id);
- a copy listing only the matching items when only its contents match —
TotalItemCountkeeps the true count, soCapacityLabelstill reads the real fill andIsNarroweddrives the "Showing N of M items" caption; -
nullwhen nothing matches.
Since v1.7.0 a card's rows are Stacks (items grouped by ItemStackKey) rather than items. Whether
a card groups follows the Identical items setting (StackingMode, stored through
IInventoryDisplayStore); a card over MaxItemsListedOneByOne (500) groups regardless. Fill
(InventoryFill) is over-full (more items than slots) or near the load limit (over 7,200), and
drives the capacity badge, the Needs attention filter and the Overview banner.
InventoriesViewModel builds the full card list once per Load() and projects it through
NarrowTo on every filter change; the prebuilt list is never modified.
KnownPlanetIds() is the ordered union of every PlanetId in the save (metadata + terraformations +
players). ResolvePlanetId(int?) maps a WorldObject.Planet hash back to one of them through
PlanetHash.Of. It backs the Inventories World filter, the Teleport landmark filter, and the
Teleport planet dropdown. See Worlds & Planets.
| ViewModel | Page |
|---|---|
OpenFileViewModel |
Open File |
OverviewViewModel (+ PlanetTerraformViewModel, PlayerOverviewRow) |
Overview |
InventoriesViewModel |
Inventories |
SelectInventoryViewModel |
"Choose a destination" modal |
LogisticsEditorViewModel |
Logistics editor modal |
TerraTokensViewModel |
Terra Tokens |
TeleportViewModel |
Teleport |
AboutViewModel |
About |
They use CommunityToolkit.Mvvm
(ObservableObject, [ObservableProperty], [RelayCommand]).
Page ViewModels are singletons and implement ViewModels/ILoadable. They re-read workspace state
in Load() rather than caching it, so switching pages always reflects the latest in-memory edits.
The shell calls Load() only when a workspace revision counter has changed since that page last
loaded — so navigating back and forth does not rebuild hundreds of inventory cards for nothing.
Every ILoadable page also takes INavigationService and exposes an OpenFileCommand for its "no
file loaded" empty state.
Presentation/VitalStatus classifies a gauge value (healthy / warning / critical) and
Presentation/StatusPalette maps a status to a theme resource key. Each UI head wraps them in a
thin IValueConverter, so the thresholds and the colour mapping live in one place and are unit
tested. Status text held in a ViewModel is stored as a localization key plus arguments and
re-formatted when the language changes.
The head implements these; Core only ever sees the interface.
| Interface | Purpose |
|---|---|
IFilePickerService |
Choose a save file |
INavigationService |
Switch pages, open / close a modal |
IDialogService |
Message and confirmation dialogs |
IScreenReaderAnnouncer |
Announce status changes to assistive technology |
IAppVersionInfo |
The running version, for About |
ILanguageStore, IDisclaimerGate
|
Persist the chosen language and the disclaimer acknowledgement |
ISaveBackupService |
Keep a copy of a save from before PCEdit first wrote to it |
INavigationService is deliberately narrow — go to Overview, go to Open File, open the
select-inventory modal, open the logistics editor, close the modal. No URL routing, no view types.
See Desktop UI for the Avalonia implementations, and Localization for
ILocalizer.
PCEdit · unofficial fan tool for The Planet Crafter · GPL-3.0-or-later · back up your saves (disclaimer)
Using PCEdit
- Installation
- Save File Locations
- Quick Start
- Worlds & Planets
- Overview & Terraforming
- Inventories
- Logistics Editor
- Terra Tokens
- Teleport
- Languages & Settings
- FAQ & Troubleshooting
Development
- Architecture
- Save File Format
- Save File Library
- App Core
- Desktop UI
- Localization
- Item Catalog
- Testing
Shipping