Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions apps/save-editor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Added

- A Trade tab shows what a merchant offers for sale and how much ore he has to
buy with. Stock counts and his ore can be changed, lines can be added and
removed, and the restock baseline is editable next to the live stock.
- An NPC can be moved, with the same location picker the hero has.
- An NPC's daily routine can be switched off, so he stays where he was put
instead of walking back within seconds. It can be switched back on again, as
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ class CharacterRow {
required this.hasInventory,
required this.hasKnowledge,
required this.hasEvents,
this.isTrader = false,
});

factory CharacterRow.fromJson(Map<String, Object?> json) {
Expand All @@ -19,6 +20,7 @@ class CharacterRow {
hasInventory: json['hasInventory'] == true,
hasKnowledge: json['hasKnowledge'] == true,
hasEvents: json['hasEvents'] == true,
isTrader: json['isTrader'] == true,
);
}

Expand All @@ -29,6 +31,9 @@ class CharacterRow {
final bool hasKnowledge;
final bool hasEvents;

/// The character runs a shop — he owns a row in the global trader array.
final bool isTrader;

bool get isOrphan => globalId == null;
}

Expand Down
89 changes: 89 additions & 0 deletions apps/save-editor/lib/features/editor/domain/editor_notifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import 'package:goresave/features/editor/domain/pending_edits.dart';
import 'package:goresave/features/editor/domain/progression_models.dart';
import 'package:goresave/features/editor/domain/skills_models.dart';
import 'package:goresave/features/editor/domain/story_state_models.dart';
import 'package:goresave/features/editor/domain/trader_models.dart';
import 'package:goresave/l10n/app_localizations.dart';
import 'package:goresave/l10n/app_localizations_en.dart';
import 'package:goresave/utils/default_paths.dart';
Expand Down Expand Up @@ -1279,6 +1280,11 @@ class EditorNotifier extends StateNotifier<EditorState> {
'private.glossary.setSegment',
'private.npc.revive',
'private.npc.setRelationship',
// Both splice a trader's stock map, which shifts every later byte offset
// and renumbers the map's entry indices. private.traders.setStock is
// deliberately absent: it overwrites a bare i32 in place, so it batches.
'private.traders.addItem',
'private.traders.removeItem',
storyStateApplyPath,
};
// A skill edit can learn/unlearn — splicing the hero's ActiveEffects array —
Expand Down Expand Up @@ -2799,6 +2805,77 @@ class EditorNotifier extends StateNotifier<EditorState> {
}
}

/// Load every merchant's shop record (`private.traders.list`).
///
/// Returns a result carrying an inline [TradersResult.error] instead of
/// throwing, and reports which trader commands this core build offers so the
/// panel degrades to read-only against an older core rather than sending a
/// command that does not exist.
Future<TradersResult> loadTraders() async {
final path = state.selectedPath;
if (path == null) {
return TradersResult(error: _l10n.editorNoSaveSelected);
}
try {
final response = await _execute(
'private.traders.list',
payload: {'path': path},
);
if (response['ok'] != true) {
return TradersResult(
error: _l10n.editorTradersLoadFailed(_errorDetails(response)),
);
}
return TradersResult.fromJson(
(response['data'] as Map).cast<String, Object?>(),
);
} catch (error) {
return TradersResult(error: _l10n.editorTradersLoadFailed('$error'));
}
}

/// Load one merchant's full record by its `m_Traders` index.
///
/// The index, not the name, is the address: two shipped rows are named `None`
/// and the core refuses to guess between them.
Future<TraderDetailResult> loadTraderDetail(int index) async {
final path = state.selectedPath;
if (path == null) {
return TraderDetailResult(error: _l10n.editorNoSaveSelected);
}
try {
final response = await _execute(
'private.traders.detail',
payload: {'path': path, 'index': index},
);
if (response['ok'] != true) {
return TraderDetailResult(
error: _l10n.editorTradersLoadFailed(_errorDetails(response)),
);
}
return TraderDetailResult(
detail: TraderDetail.fromJson(
(response['data'] as Map).cast<String, Object?>(),
),
);
} catch (error) {
return TraderDetailResult(error: _l10n.editorTradersLoadFailed('$error'));
}
}

/// Queue one trader stock change. Re-editing the same line replaces its
/// pending edit rather than stacking a second one.
void setTraderStockEdit(TraderStockEdit edit) {
setPendingEdit(
edit.pendingKey,
PendingSaveEdit(edits: [edit.toEdit()]),
);
}

/// Drop a queued trader change (the user reverted the field).
void clearTraderStockEdit(TraderStockEdit edit) =>
clearPendingEdit(edit.pendingKey);

/// Run one progression section query. Returns the raw data map, or null
/// with [onError] called, so each typed loader below can build its own page
/// object with an inline error.
Expand Down Expand Up @@ -4113,6 +4190,11 @@ bool _mayInvalidateOrdinals(Map<String, Object?> edit) {
'private.npc.setRelationship',
'private.glossary.setSegment',
'private.skills.set',
// Splice an entry into or out of a trader's stock map, which changes how
// many entries it holds. private.traders.setStock is absent: it overwrites a
// bare i32 in place.
'private.traders.addItem',
'private.traders.removeItem',
storyStateApplyPath,
}.contains(path);
}
Expand Down Expand Up @@ -4146,6 +4228,13 @@ bool _carriesCallerOrdinal(Map<String, Object?> edit) {
if (path == 'private.inventory.removeItem') {
return (value is Map ? value['slotId'] : null) != null;
}
if (path == 'private.traders.setStock' ||
path == 'private.traders.addItem' ||
path == 'private.traders.removeItem') {
// Every trader edit addresses its row by an index into the trader array that
// the user's view supplied.
return true;
}
return false;
}

Expand Down
Loading
Loading