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
38 changes: 36 additions & 2 deletions docs/roadmaps/game-area-portals.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,12 +218,46 @@ The compiled `Box2D.Portals.dll` entrypoint also passed in both default and
replay; presentation mode reported five ticks, twelve frames, twenty-four area nodes and
twenty-four points. An invalid argument printed usage and exited with code 2.

P3 remains open. Public-only rendering-closure qualification, runtime performance
counters and fresh/retained adoption still need their own evidence. Historical
At this source qualification, public-only rendering-closure qualification remained
pending; the separate fresh receiver below supplies that bounded evidence. P3 remains
open for runtime performance counters and fresh/retained product adoption. Historical
producer `0.17.0` acceptance and the retained
resume04 failure/resume05 unrun reservation are unchanged; this source attempt
does not retry those operations or transfer their custody.

### P3 fresh public presentation package receiver — 2026-10-10

The existing Box2D package-consumer runner has an explicit public `--presentation`
route and a separate exact-PackageReference project. It copies the delivered portal
presentation and actual command-line modules into a fresh outside-checkout receiver,
checks source/project inputs before restore and the expected seven-package rendering
closure before build, then requires unchanged locked restore and both entrypoint modes.
The original four-package physics consumer remains a separate no-presentation route.

Thirteen local source controls passed with synthetic assets and a stubbed `dotnet`:
both valid routes reached restore, missing/linked source and foreign direct pins or
ProjectReferences refused before restore, and missing Render, foreign packages,
project dependency nodes and disabled assertions refused at the boundary.

A separately admitted fresh public receiver then passed at source
`0a8665a9c8005f3edf2786ebd85bac311f1cd0d5`. Its new package and HTTP caches used
only nuget.org. Actual restored assets and nuspecs passed the exact seven-package
boundary: Game Core, Physics.Box2D and Render `0.17.0`, UI Scene and KeyboardInput
`0.31.0`, Box2D.NET `3.1.654`, and FSharp.Core `10.1.302`. The genuinely generated
lock remained byte-identical after locked restore. Release compilation reported no
warnings or errors. The copied actual default and presentation entrypoints exited 0;
an invalid argument printed usage and exited 2. Presentation reported five ticks,
twelve frames, twenty-four area nodes and twenty-four points.

The receiver finished in 6.224 seconds on CPU 1 with a 1.5 GiB managed heap cap;
this duration is an operation observation, not a performance qualification. A separate
post-terminal census found no remaining member of its owned process group/session;
detached descendants and total RSS were not independently measured. This proves the
bounded public package rendering closure and copied-source entrypoints, not generated
product installation or retained adoption. P3 remains open for performance and
fresh/retained product adoption. Original resume04/resume05 effects, caches and custody
are unchanged and were not reused.

A crate straddling an opening and colliding on both sides, or a rope/joint spanning
areas, requires a separately designed and measured solver/constraint strategy. Body
recreation does not preserve hidden contacts, warm-start state or exact solver rollback.
Expand Down
22 changes: 22 additions & 0 deletions tests/release/Box2D.PackageConsumer/PresentationConsumer.fsproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<ManagePackageVersionsCentrally>false</ManagePackageVersionsCentrally>
<DisableImplicitFSharpCoreReference>true</DisableImplicitFSharpCoreReference>
<DisableImplicitLibraryPacksFolder>true</DisableImplicitLibraryPacksFolder>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup>
<ItemGroup>
<Compile Include="PortalScene.fs" />
<Compile Include="Presentation.fs" />
<Compile Include="Program.fs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="FS.GG.Game.Physics.Box2D" Version="[0.17.0]" />
<PackageReference Include="FS.GG.Game.Render" Version="[0.17.0]" />
<PackageReference Include="FSharp.Core" Version="[10.1.302]" />
</ItemGroup>
</Project>
26 changes: 26 additions & 0 deletions tests/release/Box2D.PackageConsumer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,29 @@ scene against package APIs, requiring one live traveller after transfer, transfo
one step per local world per tick, one traversal, no repeated transfer and exact replay. SDK10.0.401 and runtime10.0.12 scope this result; output
reports the actual OS/architecture/runtime. Candidate smoke cannot claim public installed
support; neither smoke selects a product/template or establishes cross-platform determinism.

## Explicit package-only portal presentation

`../test-box2d-package-consumer.sh --public --presentation --output /absolute/new/directory`
selects the presentation consumer. It copies the real portal presentation and command-line
modules from `examples/Box2D.Portals`, together with the existing package-only portal scene,
into a fresh directory outside the checkout. The consumer has exact PackageReferences to
Game Physics.Box2D and Render `0.17.0` and FSharp.Core `10.1.302`; it uses no sibling projects,
repository imports or linked sources. Presentation selection requires the public feed.

Before restore, the route rejects missing or linked inputs and validates the copied project
and exact direct pins. After restore, it requires exactly seven packages: Game Core,
Physics.Box2D and Render `0.17.0`, Box2D.NET `3.1.654`, FSharp.Core `10.1.302`, and
UI Scene and KeyboardInput `0.31.0`. Missing, foreign or project dependencies refuse
before compilation. The original no-presentation route keeps its four-package physics
boundary and falling-body/portal entrypoint.

The presentation route generates a lock through public restore, checks unchanged locked
restore, and builds the copied source. It runs the actual default and `--presentation`
entrypoint modes under runtime `10.0.12`; an invalid argument must print usage and exit 2.
Presentation checks destination-only traversal snaps and ordinary interpolation over
five ticks and twelve frames. Generated locks, package inputs and invalid-argument output
remain in the new private directory. The invoking owner must separately admit acquisition
and execution, bound the process lifetime/resources, and retain failure and cleanup evidence.
Source preparation alone does not establish a successful public consumer, generated-product
adoption, retained upgrade/removal, performance, browser/GPU support or historical cleanup.
33 changes: 30 additions & 3 deletions tests/release/Box2D.PackageConsumer/verify-package-boundary.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,37 @@
from pathlib import Path
import argparse
import json
import xml.etree.ElementTree as ET

if not __debug__:
raise SystemExit('package boundary assertions require Python optimization to be disabled')
parser = argparse.ArgumentParser(description='Verify the isolated Box2D package consumer boundary.')
parser.add_argument('--presentation', action='store_true')
parser.add_argument('--preflight', action='store_true', help='Check copied project/source inputs before restore.')
args = parser.parse_args()
root = Path(__file__).resolve().parent
project = ET.parse(root / 'Consumer.fsproj').getroot()
for tag in ('ProjectReference', 'Reference', 'Import'):
assert not project.findall('.//' + tag), f'consumer must contain no {tag}'
for source in project.findall('.//Compile'):
path = (root / source.attrib['Include']).resolve()
assert path.parent == root and path.is_file(), 'consumer source must be copied and local'
path = root / source.attrib['Include']
assert not path.is_symlink() and path.resolve().parent == root and path.is_file(), 'consumer source must be copied and local'
assert 'Link' not in source.attrib, 'consumer source must not be linked'
expected_references = {'FS.GG.Game.Physics.Box2D': '[0.17.0]', 'FSharp.Core': '[10.1.302]'}
if args.presentation:
expected_references['FS.GG.Game.Render'] = '[0.17.0]'
references = project.findall('.//PackageReference')
assert len(references) == len(expected_references), 'unexpected direct package reference count'
assert {node.attrib.get('Include'): node.attrib.get('Version') for node in references} == expected_references, 'direct package pins differ'
if args.presentation:
assert [node.attrib['Include'] for node in project.findall('.//Compile')] == ['PortalScene.fs', 'Presentation.fs', 'Program.fs'], 'presentation compile inputs differ'
if args.preflight:
print('package-only preflight passed: copied local sources and exact direct package pins')
raise SystemExit(0)
assets = json.loads((root / 'obj/project.assets.json').read_text())
expected = {'FS.GG.Game.Physics.Box2D/0.17.0', 'FS.GG.Game.Core/0.17.0', 'Box2D.NET/3.1.654', 'FSharp.Core/10.1.302'}
if args.presentation:
expected |= {'FS.GG.Game.Render/0.17.0', 'FS.GG.UI.Scene/0.31.0', 'FS.GG.UI.KeyboardInput/0.31.0'}
assert set(assets['libraries']) == expected, set(assets['libraries'])
assert all(row['type'] == 'package' for row in assets['libraries'].values()), 'repository project dependency present'
packages = Path(assets['project']['restore']['packagesPath'])
Expand All @@ -21,4 +42,10 @@ def dependency_ids(package, version):
return {node.attrib['id'] for node in tree.iter() if node.tag.rsplit('}', 1)[-1] == 'dependency'}
assert dependency_ids('FS.GG.Game.Core', '0.17.0') == {'FSharp.Core'}, 'Core runtime boundary changed'
assert dependency_ids('FS.GG.Game.Physics.Box2D', '0.17.0') == {'FSharp.Core', 'FS.GG.Game.Core', 'Box2D.NET'}, 'adapter runtime boundary changed'
print('package-only boundary passed: exact four-package closure, Core BCL/FSharp.Core, adapter has no rendering dependency')
if args.presentation:
assert dependency_ids('FS.GG.Game.Render', '0.17.0') == {'FSharp.Core', 'FS.GG.Game.Core', 'FS.GG.UI.Scene', 'FS.GG.UI.KeyboardInput'}, 'Render runtime boundary changed'
assert dependency_ids('FS.GG.UI.Scene', '0.31.0') == {'FSharp.Core'}, 'Scene runtime boundary changed'
assert dependency_ids('FS.GG.UI.KeyboardInput', '0.31.0') == {'FSharp.Core', 'FS.GG.UI.Scene'}, 'KeyboardInput runtime boundary changed'
print('package-only presentation boundary passed: exact seven-package closure, no window/GPU dependency')
else:
print('package-only boundary passed: exact four-package closure, Core BCL/FSharp.Core, adapter has no rendering dependency')
40 changes: 38 additions & 2 deletions tests/release/test-box2d-package-consumer.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,22 +4,45 @@ root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
packages=""
mode=""
output=""
presentation=false
while (($#)); do
case "$1" in
--packages) packages="$(cd "$2" && pwd)"; mode=candidate; shift 2 ;;
--public) mode=public; shift ;;
--output) output="$2"; shift 2 ;;
--presentation) presentation=true; shift ;;
*) echo "unknown argument: $1" >&2; exit 2 ;;
esac
done
[[ "$mode" == candidate || "$mode" == public ]] || { echo 'select --packages DIR or --public' >&2; exit 2; }
if [[ "$presentation" == true ]]; then
[[ "$mode" == public && -z "$packages" ]] || { echo '--presentation requires --public without candidate packages' >&2; exit 2; }
for source in \
"$root/tests/release/Box2D.PackageConsumer/PresentationConsumer.fsproj" \
"$root/tests/release/Box2D.PackageConsumer/PortalScene.fs" \
"$root/tests/release/Box2D.PackageConsumer/global.json" \
"$root/tests/release/Box2D.PackageConsumer/verify-package-boundary.py" \
"$root/examples/Box2D.Portals/Presentation.fs" \
"$root/examples/Box2D.Portals/Program.fs"; do
[[ -f "$source" && ! -L "$source" ]] || { echo "missing or linked presentation input: $source" >&2; exit 2; }
done
fi
if [[ -n "$output" ]]; then
[[ "$output" == /* && ! -e "$output" ]] || { echo '--output must be a new absolute directory' >&2; exit 2; }
mkdir -m 700 "$output"
else
output="$(mktemp -d "${RUNNER_TEMP:-${TMPDIR:-/tmp}}/box2d-package-consumer.XXXXXX")"
fi
cp "$root/tests/release/Box2D.PackageConsumer/"{Consumer.fsproj,Program.fs,Scene.fs,PortalScene.fs,global.json,verify-package-boundary.py} "$output/"
boundary_args=()
if [[ "$presentation" == true ]]; then
cp "$root/tests/release/Box2D.PackageConsumer/PresentationConsumer.fsproj" "$output/Consumer.fsproj"
cp "$root/tests/release/Box2D.PackageConsumer/"{PortalScene.fs,global.json,verify-package-boundary.py} "$output/"
cp "$root/examples/Box2D.Portals/"{Presentation.fs,Program.fs} "$output/"
boundary_args=(--presentation)
export NUGET_HTTP_CACHE_PATH="$output/http-cache"
else
cp "$root/tests/release/Box2D.PackageConsumer/"{Consumer.fsproj,Program.fs,Scene.fs,PortalScene.fs,global.json,verify-package-boundary.py} "$output/"
fi
python3 - "$output/NuGet.Config" "$mode" "$packages" <<'PY'
import sys, xml.etree.ElementTree as ET
path, mode, packages = sys.argv[1:]
Expand All @@ -36,11 +59,24 @@ ET.ElementTree(root).write(path, encoding='unicode', xml_declaration=True)
PY
printf 'Box2D package-only qualification mode=%s directory=%s\n' "$mode" "$output"
cd "$output"
python3 verify-package-boundary.py "${boundary_args[@]}" --preflight
# Every attempt has a new isolated package cache and public HTTPS source mapping.
# Disable the HTTP cache instead of changing the user's global NuGet cache/configuration.
dotnet restore Consumer.fsproj --configfile NuGet.Config --packages "$output/packages" --no-cache --force-evaluate --disable-parallel
python3 verify-package-boundary.py
python3 verify-package-boundary.py "${boundary_args[@]}"
if [[ "$presentation" == true ]]; then
# Prove the genuinely generated lock survives a second restore unchanged.
cp packages.lock.json packages.lock.generated.json
dotnet restore Consumer.fsproj --configfile NuGet.Config --packages "$output/packages" --no-cache --locked-mode --force --disable-parallel
cmp packages.lock.generated.json packages.lock.json
fi
dotnet build Consumer.fsproj -c Release --no-restore -m:1 --disable-build-servers
# SDK pin and explicit runtime selection scope replay evidence to this profile.
dotnet exec --fx-version 10.0.12 bin/Release/net10.0/Consumer.dll
if [[ "$presentation" == true ]]; then
dotnet exec --fx-version 10.0.12 bin/Release/net10.0/Consumer.dll --presentation
invalid_exit=0
dotnet exec --fx-version 10.0.12 bin/Release/net10.0/Consumer.dll --invalid > invalid-argument.log 2>&1 || invalid_exit=$?
[[ "$invalid_exit" == 2 ]] && grep -qF 'Usage: Box2D.Portals [--presentation]' invalid-argument.log || { echo 'invalid presentation argument did not refuse with usage/exit 2' >&2; exit 1; }
fi
printf 'Box2D package-only qualification passed mode=%s; lock and restored inputs retained in %s\n' "$mode" "$output"
Loading