Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
825393e
perf(save): stop the typed search allocating per property
dh0er Aug 11, 2026
e051c72
perf(save): load a save once, and answer a repeat read for free
dh0er Aug 11, 2026
7e3d2e9
perf(save-editor): fetch every tab's data while the user reads the ov…
dh0er Aug 11, 2026
b237794
fix(save-editor): warm the tabs of a save that is still loading its b…
dh0er Aug 12, 2026
758a7f3
fix(save): never bind a cached response to a fingerprint it does not …
dh0er Aug 12, 2026
66d1fcd
fix(save-editor): warm the NPC roster without pre-filling its memo
dh0er Aug 12, 2026
72c29da
fix(save): lower-case a search path the way the query was lower-cased
dh0er Aug 12, 2026
3a2f023
fix(save-editor): resume a warm-up that something else interrupted
dh0er Aug 12, 2026
28d844e
fix(save-editor): re-arm an interrupted warm-up instead of waiting to…
dh0er Aug 12, 2026
39914fa
docs(save): state what the cache-store re-check does and does not prove
dh0er Aug 12, 2026
6b7291c
fix(save-editor): warm every page of the tabs fetched whole
dh0er Aug 12, 2026
369bd5d
fix(save-editor): abandon a paged warm-up when the editor moves on
dh0er Aug 12, 2026
fc2b979
fix(save): keep the typed search linear, and bound the cache by what …
dh0er Aug 12, 2026
df30bdc
fix(save): cap how many backup files a listing holds at once
dh0er Aug 12, 2026
83cd26e
fix(save-editor): make the core hold the save the user came back to
dh0er Aug 12, 2026
f7b77e5
fix(save-editor): rebuild the core-held tree after the tabs, not before
dh0er Aug 12, 2026
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
5 changes: 5 additions & 0 deletions apps/save-editor/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

- Saving is much faster: a save with eight changed values took eleven seconds
and now takes one.
- Opening a savegame is about four times faster.
- Tabs no longer load one by one. Everything they show is fetched in the
background as soon as the savegame opens, so switching tabs is immediate.
- Going back to a savegame, or to a tab already visited, no longer reloads
anything.

### Fixed

Expand Down
330 changes: 300 additions & 30 deletions apps/save-editor/lib/features/editor/domain/editor_notifier.dart

Large diffs are not rendered by default.

11 changes: 10 additions & 1 deletion apps/save-editor/lib/features/editor/ui/editor_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ class _EditorPageState extends ConsumerState<EditorPage>
// manual Settings button stays available regardless.
WidgetsBinding.instance.addPostFrameCallback((_) {
unawaited(_maybePromptLocalizationExtract());
// Covers a page that mounts with a save already inspected (a remount, a
// hot reload): the listener in build only sees LATER changes.
if (mounted) ref.read(editorProvider.notifier).prefetchTabData();
});
}

Expand Down Expand Up @@ -110,6 +113,12 @@ class _EditorPageState extends ConsumerState<EditorPage>
Widget build(BuildContext context) {
final state = ref.watch(editorProvider);
final notifier = ref.read(editorProvider.notifier);
// A save has finished loading and its tabs are now reachable: warm the
// core's caches for them in the background so the first click on a tab
// shows data instead of a spinner. Listened to rather than called inline,
// because the warm-up writes editor state (the hero id the character index
// settles) and that must not happen during a build.
ref.listen(editorProvider, (previous, next) => notifier.prefetchTabData());
final uiScale = ref.watch(uiScaleProvider);
final zoomPct = (uiScale * 100).round();
final scheme = Theme.of(context).colorScheme;
Expand Down Expand Up @@ -2008,7 +2017,7 @@ class _AllDataPanelState extends State<_AllDataPanel> {
TypedSearchResult? _result;
bool _searching = false;
int _requestSeq = 0;
int _pageSize = 50;
int _pageSize = EditorPageSize.detail;
String _activeQuery = '';
String _source = 'private';
String _kind = 'all';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ class QuestsDetail extends ConsumerStatefulWidget {
}

class _QuestsDetailState extends ConsumerState<QuestsDetail> {
static const _defaultPageSize = 50;
static const _defaultPageSize = EditorPageSize.detail;

final TextEditingController _search = TextEditingController();
// Full quest list (fetched once with a large limit, no server filters):
Expand All @@ -256,7 +256,7 @@ class _QuestsDetailState extends ConsumerState<QuestsDetail> {
QuestJournalSection? _sectionFilter;
// The core clamps a query's `limit` to 1000, so the full quest list must be
// pulled page-by-page rather than in one oversized request.
static const _fetchPageLimit = 1000;
static const _fetchPageLimit = EditorPageSize.fullList;

@override
void initState() {
Expand Down Expand Up @@ -762,7 +762,7 @@ class KnowledgeDetail extends ConsumerStatefulWidget {
}

class _KnowledgeDetailState extends ConsumerState<KnowledgeDetail> {
static const _defaultPageSize = 50;
static const _defaultPageSize = EditorPageSize.detail;

String? _selectedCharacter;
KnowledgeEntriesPage _entries = const KnowledgeEntriesPage();
Expand Down Expand Up @@ -1491,7 +1491,7 @@ class EventsDetail extends ConsumerStatefulWidget {
}

class _EventsDetailState extends ConsumerState<EventsDetail> {
static const _defaultPageSize = 50;
static const _defaultPageSize = EditorPageSize.detail;

String? _selectedCharacter;
MemoryEventsPage _events = const MemoryEventsPage();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ class _QuestJournalNotifier extends EditorNotifier {
String? group,
int offset = 0,
int limit = 100,
String? path,
}) async => ProgressionQuestPage(
quests: _quests,
total: _quests.length,
Expand Down
41 changes: 32 additions & 9 deletions apps/save-editor/test/player_events_hero_wiring_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -159,8 +159,15 @@ void main() {
),
findsWidgets,
);
// No EVENTS query specifically: other progression sections legitimately
// load in the background (the tab prefetch), but events cannot be asked
// for without an id.
expect(
core.requests.where((r) => r.command == 'query_progression'),
core.requests.where(
(r) =>
r.command == 'query_progression' &&
r.payload['section'] == 'events',
),
isEmpty,
);

Expand Down Expand Up @@ -227,8 +234,14 @@ void main() {
),
findsNothing,
);
// As above: only the events section is forbidden, and only because there
// is no id to ask with.
expect(
core.requests.where((r) => r.command == 'query_progression'),
core.requests.where(
(r) =>
r.command == 'query_progression' &&
r.payload['section'] == 'events',
),
isEmpty,
);
},
Expand All @@ -244,6 +257,18 @@ void main() {
await tester.tap(find.widgetWithText(Tab, 'Characters'));
await tester.pumpAndSettle();

int eventsQueries() => core.requests
.where(
(r) =>
r.command == 'query_progression' &&
r.payload['section'] == 'events',
)
.length;
// The player's own events legitimately load in the background (the tab
// prefetch). Count from here, so what follows measures only what
// selecting the orphan caused.
final beforeOrphan = eventsQueries();

// Select the knowledge-only orphan from the trailing "Other" group.
await tester.tap(find.text('Ghostvoice'));
await tester.pumpAndSettle();
Expand All @@ -262,14 +287,12 @@ void main() {
findsWidgets,
);
expect(find.text('Select a character to see events'), findsNothing);
// And no events query was ever issued for the orphan.
// And no events query was issued for the orphan: it has no GlobalId, so
// there is nothing to ask with.
expect(
core.requests.where(
(r) =>
r.command == 'query_progression' &&
r.payload['section'] == 'events',
),
isEmpty,
eventsQueries(),
beforeOrphan,
reason: 'selecting the orphan issued an events query',
);
},
);
Expand Down
Loading
Loading