CNA-BUG-121: ContentManager::Load<T> never calls the virtual OpenStream, so ResourceContentManager::Load<T> ignores its resource family

CNA snapshot 009d40f5  ·  Known Issues › Current bugs  ·  source links pinned to 009d40f5

✓

Evidence basis: source-verified at the pinned commit; tests exist (not executed for this page). Claims on this page were checked by reading the CNA source at commit 009d40f5; unless a sentence says otherwise, nothing here was built or executed. Nothing on this page was executed unless the Evidence section says so.

OpenStream is documented as the seam every .xnb load goes through, but the public Load template reads files with TryReadAssetBytes; only the protected ReadAsset calls it, so an OpenStream override is bypassed.

Identifier
CNA-BUG-121
Category
Bug
Subsystem
Content & XNB/CNB/CNJ
Status
Open
Verified against
CNA 009d40f5 (009d40f5dd085c4e674d3479675fac84b12b3e0a)
Severity
Medium (a triage suggestion, not a project priority)
Evidence basis
Source-verified: read at TARGET, not executed
Tests touching this area
Yes: see Current tests
Affected contract
ContentManager::Load and the protected virtual ContentManager::OpenStream; ResourceContentManager constructed with a System::Resources::ResourceManager

Expected behaviour

In XNA 4.0 Load<T> calls ReadAsset<T>, which reads through the virtual OpenStream, so a derived manager (ResourceContentManager, a pack-file or archive manager) serves the public Load<T> from its own source. CNA's header says the same of OpenStream: "every .xnb load goes through it, so an override changes where compiled content comes from", and ResourceContentManager's class comment calls it "what makes a game that embeds its compiled content work without shipping a Content directory".

Actual behaviour at TARGET

Load<T> (ContentManager.hpp) reads the .xnb tier with TryReadAssetBytes(ResolveExistingAssetPath(BuildAssetPath(name) + ".xnb")); OpenStream is called only from ReadAsset<T>. A ResourceContentManager (ResourceContentManager.cpp) has an empty root, so its Load<T>("x") looks for x.xnb in the working directory, then the .cnb and loose tiers, and never asks its ResourceManager: it throws ContentLoadException, or loads an unrelated same-named file from the working directory.

Source locations

Evidence

Checked by reading at 009d40f5; not executed. XNA's call chain taken from the decompiled XNA 4.0 ContentManager. The site's ContentManager guide and XNB guide already state that the Load<T> ladder does not route through OpenStream; CNA's own header documentation says the opposite.

Focused reproduction

Illustrative; not compiled.

System::Resources::ResourceManager resources = /* family holding "level1" as a byte[] XNB */;
ResourceContentManager content(&services, &resources);
auto level = content.Load<LevelData>("level1");
// XNA: read from the resource family. CNA: searches ./level1.xnb, .cnb and loose files,
// then throws ContentLoadException.

Current tests

ResourceContentManagerTests.cpp and ContentRuntimeContractTests.cpp drive OpenStream and ReadAsset<T> directly; no test calls the public Load<T> on a manager that overrides OpenStream.

Regression test

In ContentRuntimeContractTests, call Load<ContractValue> on the ProbeContentManager and require openStreamCalls == 1 and the served value; in ResourceContentManagerTests, require Load<ResourceValue>("fixture") to return the resource's value with no content directory present, and a second call to hit the cache.

Blast radius

Every ContentManager subclass that overrides OpenStream (resource families, pack files, archives) and every port that calls Load<T> on one. Plain managers are unaffected: the base OpenStream and Load<T> read the same files.

Workaround

In the derived manager, expose a public method that calls the protected ReadAsset<T>(name, {}) and caches the result, and call it instead of Load<T>.

The same subject is explained at several altitudes. These are the neighbouring pages at each one.

Known issues
Bug index