Skip to content
Merged
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
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,10 @@ up to **15 seconds at a time** — WebGL rendering instead of DOM markers
combined inventory and object list.
- **Edit your save** — move, copy/paste, rotate or delete whole factory
sections (with undo), then download the edited `.sav` and load it in the
game.
game. Copies travel **between saves**, too: copy, load the other save in
the same tab, paste. Copies up to 50,000 objects also go through the OS
clipboard for pasting in another browser tab; beyond what the browser's
memory can carry, the desktop app takes over.
- **Link straight to a map** — append `?url=<address-of-a-save>` to load a
hosted `.sav` automatically (e.g. a dedicated server's autosave exposed
over HTTP). The file downloads directly into your browser — never through
Expand Down
30 changes: 30 additions & 0 deletions docs/release-notes-0.1.7.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,36 @@ game's own files.

## UI fixes

- **Copying on very large saves no longer crashes the editor.** On saves
above roughly a gigabyte of decompressed data, any copy/paste could die
with "Edit failed: unreachable" in the browser: growing the save to fit
the copied objects briefly needed twice the save's memory, blowing the
browser's 4 GB WebAssembly limit. Small and medium copies now grow the
save in place (built-in headroom); bigger ones rebuild through a
compressed snapshot at ~1x memory, so pastes big and small apply
directly in the session. An edit that genuinely cannot fit under the
4 GB limit fails up front with a clear message pointing at the desktop
app — and if the browser dies mid-edit anyway, the editor reloads and
restores your earlier edits instead of leaving a dead page.
- **Copy a build from one save into another — right in the browser.** Copy,
load the other save (same tab), paste. Copies up to 50,000 objects also go
through the OS clipboard, so they paste in another browser tab too; bigger
copies stay inside the tab — mirroring them onto the OS clipboard could
stall the whole machine — so for those, cross-save means the same-tab flow
above. Above 150,000 objects the browser refuses the copy outright
(nothing is copied — a selection that size can't paste reliably inside
the browser's 4 GB limit) and points at the desktop app, which has no
such limits.
- **Hypertube entrances are back on the map** — and with them, copies of
daisy-chained builds keep their power wires. Entrances were treated as
line-only geometry, but an entrance with its default tube shape carries no
spline in the save, leaving it invisible and unselectable — so wires
between chained entrances could never travel with a copy. They now render
as regular powered buildings again.
- **Power lines are selectable** — they highlight in box selections and can
be deleted individually. For move and copy they act as riders: a wire
travels exactly when both the things it connects travel, so a selected
wire can never be dragged off a pole or pasted dangling.
- **Pipe bottleneck warnings are now scoped like belt ones.** A Mk2 pipe
feeding two Mk1 pipes through a junction no longer warns — the detector
follows the pipe line and stops at junctions that actually split or merge
Expand Down
254 changes: 191 additions & 63 deletions map/static/map/editor.js

Large diffs are not rendered by default.

15 changes: 10 additions & 5 deletions map/static/map/save_client.js
Original file line number Diff line number Diff line change
Expand Up @@ -81,9 +81,12 @@ const SaveClient = (() => {
}
};
// A crashed worker (wasm panic / OOM) leaves indeterminate state:
// reject everything and respawn fresh.
// reject everything and respawn fresh. The session is gone with it,
// so flag sessionLost -- the editor recovers by reloading the save
// and replaying its edits instead of dead-ending on "Edit failed".
w.onerror = (event) => {
const error = new Error("Save worker crashed: " + (event.message || "unknown error"));
error.sessionLost = true;
for (const entry of pending.values()) {
entry.reject(error);
}
Expand Down Expand Up @@ -443,14 +446,16 @@ const SaveClient = (() => {
memStats() {
return request({ op: "memStats" });
},
// applyEdits(ops, fromPristine, onProgress) -> Promise<payload object>.
// applyEdits(ops, fromPristine, onProgress, force) -> Promise<payload>.
// ops: array of edit-op objects (see rust editor/ops.rs). fromPristine
// replaces the whole op list (undo); otherwise ops append.
applyEdits(ops, fromPristine, onProgress) {
// replaces the whole op list (undo); otherwise ops append. force skips
// the growth dry-run refusal (recovery replay runs in a fresh
// worker already).
applyEdits(ops, fromPristine, onProgress, force) {
activeProgress = onProgress || null;
stateVersion++;
abortHandoff();
return request({ op: "applyEdits", ops, fromPristine }).then((payloadBytes) => {
return request({ op: "applyEdits", ops, fromPristine, force: !!force }).then((payloadBytes) => {
activeProgress = null;
// Every edit cycle grows and fragments the wasm heap a little;
// swapping to a fresh lean worker after each one keeps repeated
Expand Down
14 changes: 9 additions & 5 deletions map/static/map/selection.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,16 +31,20 @@ var SelectionTool = {};

var MIN_DRAG_PX = 4; // Below this the gesture is a stray right-click, not a drag.
// Line layers whose segments are real editable actors: belts/pipes
// (per-mark buckets "line:belt:Mk.6", ...), railroads, hypertubes, and
// vehicle paths ("line:vehiclePath:<mark>"). Their spline data is
// actor-local, so the edit engine's header-transform move/copy carries
// the geometry. Power lines stay out: wires already travel with their
// poles on copy/move, and selecting them directly would double-count.
// (per-mark buckets "line:belt:Mk.6", ...), railroads, hypertubes,
// vehicle paths ("line:vehiclePath:<mark>"), and power lines. Their
// spline data is actor-local, so the edit engine's header-transform
// move/copy carries the geometry. Power lines are selectable for
// visibility and single-wire deletion, but the engine treats them as
// RIDERS of their endpoint owners: they move/copy only when both owners
// do, and a selected wire without both owners is dropped from copies
// (editor/apply.rs expand_duplicate_set / the move wire pass).
function isSelectableLineBucket(bucket) {
return bucket.key.indexOf("line:belt:") === 0
|| bucket.key.indexOf("line:pipe:") === 0
|| bucket.key === "line:railroads"
|| bucket.key === "line:hypertubes"
|| bucket.key === "line:powerLines"
|| bucket.key.indexOf("line:vehiclePath:") === 0;
}

Expand Down
2 changes: 1 addition & 1 deletion map/static/map/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,7 @@ self.onmessage = async (event) => {
const opsJson = JSON.stringify(msg.ops);
const payload = msg.fromPristine
? session.apply_edits_from_pristine(opsJson, pristine, progressCb)
: session.apply_edits(opsJson, progressCb);
: session.apply_edits(opsJson, !!msg.force, progressCb);
reply(id, payload, [payload.buffer]);
return;
}
Expand Down
19 changes: 16 additions & 3 deletions rust_parser/core/src/decompress.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,17 @@ struct Chunk {
uncomp_len: usize,
}

/// Extra capacity allocated with every decompressed body so editor inserts
/// (copies grow the body) extend IN PLACE instead of reallocating. A
/// reallocation of a body at exact capacity means a second full-body-sized
/// contiguous block lives while the first is copied out -- ~2x the body
/// transiently, which is exactly what trapped the 4GB-capped wasm build when
/// copying on a save with a 1.3GB body. 64MB covers ~50k copied objects;
/// past the slack, wasm builds rebuild through apply_plan's compressed
/// snapshot (~1x peak) and native builds take the plain realloc (plenty of
/// RAM). Also covers the 4-byte "Missing final array count" quirk pad.
pub const BODY_EDIT_SLACK: usize = 64 << 20;

/// Mirrors decompressSaveFile(offset, data) including its confirm checks.
/// `progress(bytes_of_file_consumed, total_file_bytes)` fires as chunks finish.
pub fn decompress_save_file(
Expand Down Expand Up @@ -80,16 +91,18 @@ pub fn decompress_save_file(
// panics with "capacity overflow" (an uncatchable wasm trap -> the app
// showed a raw "unreachable") the moment the body exceeds ~2.14GB. Return
// the actionable error instead -- the desktop app has no such limit. The
// +8 leaves room for the editor's quirk-padding append.
// BODY_EDIT_SLACK term keeps the allocation below (body + edit headroom)
// inside the same bound.
#[cfg(target_pointer_width = "32")]
if total_uncomp as u64 + 8 > isize::MAX as u64 {
if total_uncomp as u64 + BODY_EDIT_SLACK as u64 > isize::MAX as u64 {
return Err(perr!(
"This save decompresses to {:.2} GB. The browser build can load saves up to about 2 GB; use the desktop app for larger ones.",
total_uncomp as f64 / 1e9
));
}

let mut out: Vec<u8> = vec![0u8; total_uncomp];
let mut out: Vec<u8> = Vec::with_capacity(total_uncomp + BODY_EDIT_SLACK);
out.resize(total_uncomp, 0);

// Carve the output into per-chunk slices for parallel inflation.
let mut slices: Vec<(&Chunk, &mut [u8])> = Vec::with_capacity(chunks.len());
Expand Down
Loading
Loading