diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4982794..0ff98db 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
+## [0.4.0] - 2026-08-10
+
+A reach release. The library itself is unchanged — no type, member or behaviour
+differs from 0.3.0, and package validation against it reports no difference —
+and what is new is where it runs: the packages now carry Android and iOS
+alongside the six desktop runtimes. That is a minor version rather than a patch
+because a platform is a capability, and nothing here is a fix.
+
### Added
- **Android and iOS.** The packages now carry native binaries for
@@ -40,11 +48,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Both are verified in CI against the packed `.nupkg` rather than the
repository: a real Android application is built and its `.apk` opened to
confirm `libbox3d.so` is inside it, and a real iOS application is built and
- checked for evidence that the package handed Box3D's archive to the linker.
- A clean build proves nothing on iOS by itself — a P/Invoke to `__Internal` is
- resolved at run time, so an application the archive never reached builds and
- launches like a correct one. Neither runs a simulation on a device, and the
- platform table in the README says so rather than implying otherwise.
+ its executable read with `nm` to confirm Box3D's own native symbols are
+ defined in it — 415 of them, in the run this release was cut from. A clean
+ build proves nothing on iOS by itself: a P/Invoke to `__Internal` is resolved
+ at run time, so an application the archive never reached builds and launches
+ like a correct one.
+
+ Only symbols named `_b3…` are counted as evidence of the link. An AOT-compiled
+ method carries its parameter types in its mangled name, so `b3BodyId` and
+ `b3ShapeDef` occur inside managed symbols such as
+ `_Box3D_NET_Box3D_Body__ctor_Box3D_Native_b3BodyId`, which are in the binary
+ whether or not the archive survived; counting those would have counted the
+ wrong thing. They are counted separately instead, because they answer the
+ other question — whether the trimmer kept Box3D's C# at all — and that
+ distinction is what tells the two failures apart.
+
+ Neither platform runs a simulation on a device, and the platform table in the
+ README says so rather than implying otherwise.
### Changed
@@ -450,6 +470,8 @@ rather than left standing:
follow Box3D and are documented rather than corrected.
- Single precision only. Box3D's large-world mode changes the ABI and would need
a separate package.
-[Unreleased]: https://github.com/Miguel249/Box3D.NET/compare/v0.2.0...HEAD
+[Unreleased]: https://github.com/Miguel249/Box3D.NET/compare/v0.4.0...HEAD
+[0.4.0]: https://github.com/Miguel249/Box3D.NET/compare/v0.3.0...v0.4.0
+[0.3.0]: https://github.com/Miguel249/Box3D.NET/compare/v0.2.0...v0.3.0
[0.2.0]: https://github.com/Miguel249/Box3D.NET/compare/v0.1.0...v0.2.0
[0.1.0]: https://github.com/Miguel249/Box3D.NET/releases/tag/v0.1.0
diff --git a/Directory.Build.props b/Directory.Build.props
index fe02413..3a22a93 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -71,7 +71,7 @@
- 0.3.0
+ 0.4.0
Box3D.NET contributors
Box3D.NET
Box3D.NET
diff --git a/Directory.Build.targets b/Directory.Build.targets
index 87b0955..42ed680 100644
--- a/Directory.Build.targets
+++ b/Directory.Build.targets
@@ -56,10 +56,13 @@
This was still 0.1.0 while 0.3.0 was being prepared, which is a gap rather
than extra strictness: comparing against 0.1.0 says nothing about whether
a member 0.2.0 added has survived, so anything introduced and then removed
- inside the 0.2.x line would have passed unremarked.
+ inside the 0.2.x line would have passed unremarked. It was then left at
+ 0.2.0 while 0.3.0 shipped, which is the same gap again, so the instruction
+ above is worth taking literally: this line moves in the commit that moves
+ VersionPrefix, not in the one after it.
-->
true
- 0.2.0
+ 0.3.0
diff --git a/README.md b/README.md
index 3a26f54..e69453c 100644
--- a/README.md
+++ b/README.md
@@ -734,17 +734,27 @@ start an application on a phone, so those two words are the honest ceiling:
it for `arm64-v8a`. That covers the failure mode specific to Android: an
unresolved runtime asset produces a perfectly valid application that dies on
its first physics call.
-- **linked** — CI builds a real iOS application against the packed `.nupkg` and
- confirms the package handed Box3D's archive to the linker. That is the failure
- mode specific to iOS: the library is linked into the application rather than
- loaded, and a P/Invoke to `__Internal` is resolved by Mono at run time, so an
- application the archive never reached builds and launches exactly like a
- correct one and fails on its first physics call.
-
- The stronger check — Box3D's symbols in the finished executable — is attempted
- and normally cannot run, because a release build for iOS is stripped and
- defines no symbols at all. Where the binary does keep a symbol table and Box3D
- is missing from it, that is a failure rather than a shrug.
+- **linked** — CI builds a real iOS application against the packed `.nupkg`,
+ then reads the executable it produced with `nm` and requires Box3D's own
+ native symbols to be defined in it. That is the failure mode specific to iOS:
+ the library is linked into the application rather than loaded, and a P/Invoke
+ to `__Internal` is resolved by Mono at run time, so an application the archive
+ never reached builds and launches exactly like a correct one and fails on its
+ first physics call.
+
+ Only symbols named `_b3…` count, since those can only have come out of
+ `libbox3d.a`. An AOT-compiled method carries its parameter types in its
+ mangled name, so `b3BodyId` and `b3ShapeDef` turn up inside managed symbols
+ like `_Box3D_NET_Box3D_Body__ctor_Box3D_Native_b3BodyId`, which are in the
+ binary whether or not the archive survived the link. Those are counted too,
+ separately, because they answer the other question — whether the trimmer kept
+ Box3D's C# at all — and between them they say which half to go and look at
+ when the check fails.
+
+ A stripped executable would leave nothing to read, and the check falls back to
+ the build log's evidence that the linker was handed the archive, reporting
+ that it did so rather than claiming the stronger result. That has not happened
+ yet: the release simulator build keeps its symbol table.
Neither runs a simulation on a device. If you ship Box3D.NET on a phone, test on
a phone.