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
30 changes: 15 additions & 15 deletions .github/workflows/publish-unsigned-preview.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ on:
description: New prerelease tag; an existing release is never overwritten
required: true
type: string
default: v0.5.0-beta.19
default: v0.5.0-beta.20
preview_title:
description: Public prerelease title
required: true
type: string
default: Parlyn Engine v0.5.0 Beta 19 (unsigned)
default: Parlyn Engine v0.5.0 Beta 20 (unsigned)

permissions:
contents: write
Expand All @@ -24,8 +24,8 @@ jobs:
env:
CSC_IDENTITY_AUTO_DISCOVERY: false
GH_TOKEN: ${{ github.token }}
PREVIEW_TAG: ${{ inputs.preview_tag || 'v0.5.0-beta.19' }}
PREVIEW_TITLE: ${{ inputs.preview_title || 'Parlyn Engine v0.5.0 Beta 19 (unsigned)' }}
PREVIEW_TAG: ${{ inputs.preview_tag || 'v0.5.0-beta.20' }}
PREVIEW_TITLE: ${{ inputs.preview_title || 'Parlyn Engine v0.5.0 Beta 20 (unsigned)' }}

steps:
- name: Checkout
Expand Down Expand Up @@ -55,7 +55,7 @@ jobs:
throw "Expected exactly one Parlyn installer."
}

$portable = "release/Parlyn-Engine-Portable-0.5.0-beta.19-x64.zip"
$portable = "release/Parlyn-Engine-Portable-0.5.0-beta.20-x64.zip"
if (-not (Test-Path -LiteralPath "release/win-unpacked/Parlyn Engine.exe")) {
throw "Packaged Parlyn executable was not found for the portable build."
}
Expand All @@ -72,14 +72,14 @@ jobs:
shell: pwsh
run: |
@"
# Parlyn Engine v0.5.0 Beta 19

This unsigned Windows beta adds optional scene and world bounds as
world-space authoring guides through the Bounds command. Scene bounds
use normal saving and Undo/Redo; world bounds are explicitly saved.
Bounds do not constrain objects, cameras or runtime physics.
Beta 18 passed the maintainer's full editor regression test; Beta 19
awaits human Windows acceptance. Older editors discard bounds on save.
# Parlyn Engine v0.5.0 Beta 20

This unsigned Windows beta fixes saved Undo/Redo disappearing after
switching project scenes. History is stored independently per scene.
It retains Beta 19's optional scene and world authoring bounds.
Beta 19 was not accepted due to the history defect; Beta 20 awaits
human Windows acceptance. Previously overwritten history cannot be
reconstructed. Older editors discard bounds on save.
The portable ZIP creates no Windows installation or uninstall entry and
remains the preferred unsigned test path.

Expand All @@ -96,7 +96,7 @@ jobs:
other public certificate provider.

Maintainer acceptance instructions:
https://github.com/WebCrew/Parlyn-Engine/blob/main/docs/MAINTAINER-ACCEPTANCE-v0.5.0-beta.19.md
https://github.com/WebCrew/Parlyn-Engine/blob/main/docs/MAINTAINER-ACCEPTANCE-v0.5.0-beta.20.md
"@ | Set-Content -LiteralPath release/PREVIEW-NOTES.md -Encoding utf8

- name: Publish GitHub pre-release
Expand All @@ -114,6 +114,6 @@ jobs:
--notes-file release/PREVIEW-NOTES.md `
--prerelease `
"$($installer[0].FullName)" `
"release/Parlyn-Engine-Portable-0.5.0-beta.19-x64.zip" `
"release/Parlyn-Engine-Portable-0.5.0-beta.20-x64.zip" `
"release/latest.yml" `
"release/SHA256SUMS.txt"
8 changes: 8 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@

## Unreleased — Foundation Stabilization

- Beta 20: fixed project scenes overwriting one shared history file. Saved
Undo/Redo now uses a separate hashed path per scene, with read-only legacy
fallback and unchanged stale-snapshot/size validation.
- Added a real file-system regression test for A/B switching, same-name scenes
in different folders, saved Redo, legacy history and corrupt/stale rejection.
- Beta 19 remains unaccepted following the maintainer's history defect report;
Beta 20 awaits re-testing. Already overwritten history cannot be recovered.

- Beta 19: added optional validated scene/world authoring bounds, independent
of viewport size, with separate blue/amber guides and explicit world saving.
- Added scene bounds Undo/Redo and round-trip/validation/helper-lifecycle tests.
Expand Down
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@

# Parlyn Engine

Beta 19 adds optional scene/world bounds through **Bounds** in the View command
Beta 20 fixes saved per-scene Undo/Redo history after a scene switch. It retains
Beta 19's optional scene/world bounds through **Bounds** in the View command
group: blue scene guides and amber world guides in world coordinates. They do
not restrict camera movement, objects or physics. Scene edits use Undo/Redo and
normal Save; world edits use explicit Save World Bounds. Human Beta 19 Windows
acceptance is pending. Beta 18 passed the full maintainer editor regression test.
normal Save; world edits use explicit Save World Bounds. Beta 19 was not accepted
due to the history defect; Beta 20 human Windows acceptance is pending.
Beta 18 passed the full maintainer editor regression test.

**Parlyn Engine** is a free and open-source game engine designed primarily for **modern 2.5D development**, while keeping real 3D available wherever a project benefits from it.

Expand Down Expand Up @@ -235,7 +237,7 @@ See [`docs/WINDOWS-DISTRIBUTION.md`](docs/WINDOWS-DISTRIBUTION.md) for signing,
verification and the maintainer acceptance test.

Phase 1 acceptance is complete. The current Phase 2 editor candidate uses the
repeatable [`v0.5.0-beta.19 maintainer checklist`](docs/MAINTAINER-ACCEPTANCE-v0.5.0-beta.19.md).
repeatable [`v0.5.0-beta.20 maintainer checklist`](docs/MAINTAINER-ACCEPTANCE-v0.5.0-beta.20.md).

### Windows release integrity

Expand Down
4 changes: 4 additions & 0 deletions docs/MAINTAINER-ACCEPTANCE-v0.5.0-beta.19.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
Status: pending human Windows acceptance. Beta 18 passed the maintainer's full
editor regression test on 2026-09-12.

Beta 19 was not accepted: the maintainer found saved Undo history disappearing
after switching project scenes. Beta 20 addresses the shared-history-file cause
and requires the dedicated per-scene history re-test before acceptance.

1. Open an existing project. Bounds are initially absent. Open **Bounds** in
the View command group; check tooltips, keyboard focus and readable labels.
2. Enable Current Scene bounds, enter Min (-10,-3,-10), Max (10,8,10), and
Expand Down
27 changes: 27 additions & 0 deletions docs/MAINTAINER-ACCEPTANCE-v0.5.0-beta.20.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Parlyn Engine v0.5.0 Beta 20 — Maintainer Acceptance

Status: pending human Windows acceptance. Beta 19 was not accepted because
saving a second project scene overwrote the first scene's local history.
Beta 20 separates saved history by project-relative scene path.

Use a disposable copy of a project with two scenes.

1. In scene A enable/change Bounds, Apply and Save. Undo must remain enabled.
2. Switch to B, enable different Bounds, Apply and Save.
3. Switch back to A. Undo must still be enabled and undo A's Bounds edit only.
4. Redo restores A's exact values. Save A, switch to B: B's Undo must still work
independently. Repeat A/B switching several times.
5. Undo an edit, Save, switch away and return: Redo must remain available if no
new edit was made. A new edit after Undo must clear only that scene's Redo.
6. Save both scenes, close the project and Engine, restart and reopen. Each
saved scene must retain its own Undo/Redo state. Include transform edits.
7. Test scene names that are identical but live in different subfolders.
8. Repeat the Bounds checks in [Beta 19's checklist](MAINTAINER-ACCEPTANCE-v0.5.0-beta.19.md), including loose-scene persistence and World Bounds.
9. Re-test unsaved Cancel/Discard/Save & Continue, snapping, Ground/End,
Frame Selected/F, multi-selection and panel restoration.

History reflects its last successful save, not unsaved actions intentionally
discarded during switching. Already overwritten Beta 19 history cannot be
reconstructed by this fix. A matching legacy history remains readable until
the next save writes a separate per-scene file. Renamed/moved scene paths
have separate history identities; stale history is never attached blindly.
9 changes: 7 additions & 2 deletions docs/PROJECT-FORMAT.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,13 @@ Contains source assets imported by the developer. External asset formats are not
## `.parlyn/`

Reserved for generated caches, import metadata and other project-local internal
data. Beta 5 stores the validated startup Scene History here as
`startup-scene.parlyn-history.json`. Developers should not rely on internal
data. Beta 20 stores validated per-scene History under
`scene-history/<sha256-of-project-relative-scene-path>.parlyn-history.json`.
The previous shared `startup-scene.parlyn-history.json` is read-only legacy
fallback, accepted only for its matching scene and snapshot. New saves never
overwrite that legacy file. History remains limited to 100 entries and the
existing size limits. Renamed/moved paths have separate history identities.
Developers should not rely on internal
paths as a public API; the history document itself remains explicitly versioned.

## Compatibility contract v1
Expand Down
1 change: 1 addition & 0 deletions docs/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ Completion evidence:

- duplication and reparenting — initial editor commands complete;
- durable, validated local Scene History — initial foundation complete;
Beta 20 corrects saved per-scene history isolation; human re-test pending;
- multi-selection where appropriate — initial hierarchy and viewport selection complete;
- multi-scene project browser — validated listing, switching, creation, rename and move complete;
- rename and move project assets — initial safe editor workflow complete;
Expand Down
2 changes: 1 addition & 1 deletion docs/TESTING-v0.5.0.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Parlyn Engine v0.5.0 Test Guide

For the current Phase 2 editor acceptance pass, use the shorter artifact-specific
[`MAINTAINER-ACCEPTANCE-v0.5.0-beta.19.md`](MAINTAINER-ACCEPTANCE-v0.5.0-beta.19.md)
[`MAINTAINER-ACCEPTANCE-v0.5.0-beta.20.md`](MAINTAINER-ACCEPTANCE-v0.5.0-beta.20.md)
The guide below remains the detailed feature reference.

This test focuses on the two new foundations introduced in v0.5.0: viewport transform gizmos and the module lifecycle.
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "parlyn-engine",
"version": "0.5.0-beta.19",
"version": "0.5.0-beta.20",
"private": true,
"description": "Parlyn Engine - open-source 2.5D-first game engine editor",
"author": "Parlyn Engine contributors",
Expand Down
6 changes: 5 additions & 1 deletion scripts/check-desktop-boundaries.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,11 @@ const { resolveExistingProjectPath, resolveWritableProjectPath, resolveWritableP
assert.match(main, /secureHandle\('parlyn:project:move-asset'/);
assert.match(main, /secureHandle\('parlyn:project:close'/);
assert.match(main, /secureHandle\('parlyn:project:delete'/);
assert.match(main, /parlyn-scene-history/);
const sceneHistoryFiles = await fs.readFile(path.join(__dirname, '../src/main/sceneHistoryFiles.mjs'), 'utf8');
assert.match(main, /import\('\.\/sceneHistoryFiles\.mjs'\)/);
assert.match(main, /\.loadSceneHistory\(projectRoot, sceneRelativePath, currentScene\)/);
assert.match(main, /\.saveSceneHistory\(activeProjectRoot, relativePath, payload\.scene, payload\.history\)/);
assert.match(sceneHistoryFiles, /parlyn-scene-history/);
assert.match(renderer, /history\.exportState\(/);
assert.match(renderer, /history\.restoreState\(result\.history\)/);
assert.match(renderer, /normalizeWorkspaceLayout/);
Expand Down
65 changes: 65 additions & 0 deletions scripts/check-scene-history-files.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import assert from 'node:assert/strict';
import fs from 'node:fs/promises';
import os from 'node:os';
import path from 'node:path';
import { SceneDocument } from '../src/engine/scene/SceneDocument.mjs';
import { History } from '../src/engine/history/History.mjs';
import { sceneHistoryRelativePath, saveSceneHistory, loadSceneHistory } from '../src/main/sceneHistoryFiles.mjs';
import { readDocumentFile, writeDocumentFileAtomic } from '../src/main/documentFiles.mjs';

const root = await fs.mkdtemp(path.join(os.tmpdir(), 'parlyn-per-scene-history-'));
try {
const emptyRoot = path.join(root, 'empty');
await fs.mkdir(emptyRoot);
assert.deepEqual(await loadSceneHistory(emptyRoot, 'scenes/new.json', new SceneDocument().toJSON()), { history:null, warning:null });
const entries = ['scenes/first/Demo.parlyn-scene.json', 'scenes/second/Demo.parlyn-scene.json'].map((scenePath, index) => {
const scene = new SceneDocument(`Demo ${index}`);
const before = scene.toJSON();
scene.bounds = { min:{ x:-10-index, y:-3, z:-10 }, max:{ x:10+index, y:8, z:10 } };
const history = new History(); history.push(before, 'Change Scene Bounds');
return { scenePath, scene, before, history };
});
assert.notEqual(sceneHistoryRelativePath(entries[0].scenePath), sceneHistoryRelativePath(entries[1].scenePath));
for (const entry of entries) {
const file = path.join(root, entry.scenePath);
await fs.mkdir(path.dirname(file), { recursive:true });
await writeDocumentFileAtomic(file, entry.scene.toJSON(), 'parlyn-scene');
await saveSceneHistory(root, entry.scenePath, entry.scene.toJSON(), entry.history.exportState());
}
// Save A, save B, reopen A, reopen B: each scene retains its own Undo.
for (const entry of entries) {
const scene = await readDocumentFile(path.join(root, entry.scenePath), 'parlyn-scene');
const result = await loadSceneHistory(root, entry.scenePath, scene);
assert.equal(result.warning, null);
const restored = new History(); restored.restoreState(result.history);
assert.equal(restored.canUndo, true);
assert.deepEqual(restored.undo(scene).snapshot, entry.before);
assert.deepEqual(restored.redo(entry.before).snapshot, scene);
}
// Persist an Undo followed by a switch/reopen: Redo is retained too.
const first = entries[0];
first.history.undo(first.scene.toJSON());
await saveSceneHistory(root, first.scenePath, first.before, first.history.exportState());
const redoResult = await loadSceneHistory(root, first.scenePath, first.before);
const redoHistory = new History(); redoHistory.restoreState(redoResult.history);
assert.equal(redoHistory.canRedo, true);
assert.deepEqual(redoHistory.redo(first.before).snapshot, first.scene.toJSON());
assert.equal((await loadSceneHistory(root, first.scenePath, first.scene.toJSON())).history, null, 'Stale history remains rejected.');
assert.throws(() => sceneHistoryRelativePath('../outside.json'));

// Legacy history is read-only, accepted only when path and snapshot match.
const legacy = path.join(root, '.parlyn/startup-scene.parlyn-history.json');
await writeDocumentFileAtomic(legacy, { format:'parlyn-scene-history', version:1,
scenePath:'scenes/legacy.json', currentScene:first.before, history:new History().exportState(), updatedAt:new Date().toISOString() }, 'parlyn-scene-history');
assert.notEqual((await loadSceneHistory(root, 'scenes/legacy.json', first.before)).history, null);
assert.equal((await loadSceneHistory(root, 'scenes/other.json', first.before)).history, null);
const legacyBefore = await fs.readFile(legacy, 'utf8');
await saveSceneHistory(root, 'scenes/legacy.json', first.before, new History().exportState());
assert.equal(await fs.readFile(legacy, 'utf8'), legacyBefore, 'New saves must not overwrite legacy history.');
const primary = path.join(root, sceneHistoryRelativePath('scenes/legacy.json'));
await fs.writeFile(primary, '{broken', 'utf8');
assert.equal((await loadSceneHistory(root, 'scenes/legacy.json', first.before)).history, null, 'Corrupt primary must not fall back to an older legacy snapshot.');
} finally {
await fs.rm(root, { recursive:true, force:true });
}
console.log('Per-scene saved Undo/Redo history regression check passed.');
2 changes: 1 addition & 1 deletion scripts/check-windows-distribution.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ requireValue(workflow.includes('Verify signed Windows artifacts'), 'Signed outpu
requireValue(workflow.includes('require_signing'), 'Windows workflow must distinguish signed and unsigned preflight builds.');
requireValue(!workflow.includes('BEGIN PRIVATE KEY'), 'Signing material must never be embedded in the workflow.');
requireValue(previewWorkflow.includes('Compress-Archive'), 'Unsigned previews must create a portable ZIP test artifact.');
requireValue(previewWorkflow.includes('Parlyn-Engine-Portable-0.5.0-beta.19-x64.zip'), 'Portable preview artifact has an unexpected name.');
requireValue(previewWorkflow.includes('Parlyn-Engine-Portable-0.5.0-beta.20-x64.zip'), 'Portable preview artifact has an unexpected name.');
requireValue(previewWorkflow.includes('release/win-unpacked/Parlyn Engine.exe'), 'Portable preview must verify its packaged executable.');

const gitignore = fs.readFileSync(path.join(root, '.gitignore'), 'utf8');
Expand Down
6 changes: 5 additions & 1 deletion scripts/check.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,9 @@ const required = [
'scripts/check-transform-space.mjs',
'scripts/check-frame-selected.mjs',
'scripts/check-document-bounds.mjs',
'scripts/check-scene-history-files.mjs',
'src/main/sceneHistoryFiles.mjs',
'docs/MAINTAINER-ACCEPTANCE-v0.5.0-beta.20.md',
'src/engine/core/DocumentBounds.mjs',
'docs/MAINTAINER-ACCEPTANCE-v0.5.0-beta.19.md',
'scripts/check-asset-files.cjs',
Expand All @@ -78,7 +81,7 @@ for (const rel of required) {

const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
if (pkg.name !== 'parlyn-engine') throw new Error('Unexpected package name.');
if (pkg.version !== '0.5.0-beta.19') throw new Error(`Unexpected package version: ${pkg.version}`);
if (pkg.version !== '0.5.0-beta.20') throw new Error(`Unexpected package version: ${pkg.version}`);

function walk(dir) {
return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
Expand All @@ -105,6 +108,7 @@ cp.execFileSync(process.execPath, [path.join(root, 'scripts/check-transform-snap
cp.execFileSync(process.execPath, [path.join(root, 'scripts/check-transform-space.mjs')], { stdio: 'inherit' });
cp.execFileSync(process.execPath, [path.join(root, 'scripts/check-frame-selected.mjs')], { stdio: 'inherit' });
cp.execFileSync(process.execPath, [path.join(root, 'scripts/check-document-bounds.mjs')], { stdio: 'inherit' });
cp.execFileSync(process.execPath, [path.join(root, 'scripts/check-scene-history-files.mjs')], { stdio: 'inherit' });
cp.execFileSync(process.execPath, [path.join(root, 'scripts/check-asset-files.cjs')], { stdio: 'inherit' });
cp.execFileSync(process.execPath, [path.join(root, 'scripts/check-windows-distribution.cjs')], { stdio: 'inherit' });

Expand Down
Loading
Loading