Skip to content

Commit bfc826e

Browse files
committed
feat(SAMPLE-062): support streamed particle XML
1 parent 2499b75 commit bfc826e

6 files changed

Lines changed: 76 additions & 6 deletions

File tree

docs/XmlSerializationScope.md

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
# `System.Xml.Serialization` — scope, evidence, and known deviations
22

33
Ticket: `SAMPLES-DEC-008`. Module: `modules/xml-serialization` (`Xml.Serialization`, header-only,
4-
`INTERFACE`, depends on `Core.Base` and `Xml`).
4+
`INTERFACE`, depends on `Collections.Core`, `IO`, and `Xml`).
55

6-
This module exists to unblock three `cna-samples` ports — `SAMPLE-014` (Spacewar),
7-
`SAMPLE-066` (ShipGame) and `SAMPLE-070` (RolePlayingGame) — which the owner marked blocked on
6+
This module exists to unblock four `cna-samples` ports — `SAMPLE-014` (Spacewar),
7+
`SAMPLE-062` (NetRumble), `SAMPLE-066` (ShipGame), and `SAMPLE-070` (RolePlayingGame) — which the owner marked blocked on
88
2026-08-28 with an explicit instruction not to add another hand-written per-sample XML parser.
99
It is a shared, generic engine, not a parser for any one sample.
1010

@@ -53,6 +53,7 @@ inside a local class, so a type declared inside a function body will not compile
5353
| `INF`/`-INF`/`NaN` schema tokens, not .NET's `Infinity` spelling | XML Schema's `float` lexical space |
5454
| Markup escaping (`&`, `<`, `>`, quotes) and non-ASCII text | a quest named `Smith & Son` must not corrupt a save |
5555
| **Nested serialization into a caller's document** (`SerializeInto`/`DeserializeFrom`/`RootElementName`) | **16 of the 20** `Session.cs` call sites serialize into an already-open `XmlWriter` |
56+
| Deserialization from a readable `Stream` at its current position without taking ownership | NetRumble's `ParticleEffect.Load(ContentManager, String)` opens each particle XML with `File.OpenRead` and passes the stream to `XmlSerializer.Deserialize` |
5657
5758
## The dominant call-site shape: nesting, not standalone documents
5859
@@ -99,12 +100,12 @@ Each of these is **absent by evidence, not by omission**:
99100
belongs to the **Content Pipeline** writers (`CharacterWriter.cs`,
100101
`FightingCharacterWriter.cs` in `RolePlayingGameProcessors`), which build `.xnb` at build time
101102
and never reach `XmlSerializer`.
102-
- **`[XmlArray]` / `[XmlArrayItem]` naming overrides.** No occurrences in the three samples.
103+
- **`[XmlArray]` / `[XmlArrayItem]` naming overrides.** No occurrences in the four samples.
103104
- **`[XmlAttribute]`-mapped members.** Every reachable member is element-mapped.
104105
- **Circular-reference detection.** The reachable save graphs are trees and lists, with content
105106
referenced by asset *name* rather than by object identity.
106107

107-
If any of these turns up in a fourth sample, it is new work with its own ticket — not a silent
108+
If any of these turns up in a fifth sample, it is new work with its own ticket — not a silent
108109
gap here.
109110

110111
## Known deviations

modules/io/include/System/IO/DirectoryInfo.hpp

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
#include <filesystem>
66
#include <string>
77
#include <vector>
8+
#include "System/IO/Directory.hpp"
89
#include "System/IO/DirectoryNotFoundException.hpp"
910
#include "System/IO/FileInfo.hpp"
1011
#include "System/IO/FileSystemInfo.hpp"
@@ -157,6 +158,24 @@ namespace System::IO {
157158
return result;
158159
}
159160

161+
/**
162+
* @brief Returns FileInfo objects for files whose names match the search pattern.
163+
*
164+
* @param searchPattern Pattern containing the same wildcard syntax accepted by
165+
* Directory::GetFiles.
166+
* @return Matching files in the directory.
167+
* @throws System::IO::DirectoryNotFoundException if the directory does not exist.
168+
* @throws System::UnauthorizedAccessException if enumeration is denied.
169+
* @throws System::IO::IOException for another filesystem enumeration failure.
170+
*/
171+
[[nodiscard]] std::vector<FileInfo> GetFiles(const std::string& searchPattern) const {
172+
std::vector<FileInfo> result;
173+
for (const std::string& path : Directory::GetFiles(fullPath_.string(), searchPattern)) {
174+
result.emplace_back(path);
175+
}
176+
return result;
177+
}
178+
160179
/**
161180
* @brief Returns a list of file paths for the files in this directory.
162181
* @throws System::IO::DirectoryNotFoundException if this directory does not exist.

modules/io/tests/System/IO/IOStreamTests.cpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -758,6 +758,24 @@ TEST(DirectoryInfoTests, GetFiles_NonExistent_ThrowsDirectoryNotFoundException)
758758
EXPECT_THROW((void)di.GetFiles(), System::IO::DirectoryNotFoundException);
759759
}
760760

761+
TEST(DirectoryInfoTests, GetFiles_SearchPattern_ReturnsMatchingFileInfoObjects) {
762+
const std::string dir = tf("di_getfiles_pattern");
763+
Directory::CreateDirectory(dir);
764+
File::WriteAllText(dir + "/first.xnb", "xnb");
765+
File::WriteAllText(dir + "/SECOND.XNB", "xnb");
766+
File::WriteAllText(dir + "/ignored.wav", "wav");
767+
768+
const DirectoryInfo info(dir);
769+
const auto files = info.GetFiles("*.xnb");
770+
771+
ASSERT_EQ(files.size(), 2u);
772+
EXPECT_TRUE(files[0].getNameProperty() == "first.xnb" ||
773+
files[1].getNameProperty() == "first.xnb");
774+
EXPECT_TRUE(files[0].getNameProperty() == "SECOND.XNB" ||
775+
files[1].getNameProperty() == "SECOND.XNB");
776+
Directory::Delete(dir, true);
777+
}
778+
761779
// ===========================================================================
762780
// BinaryWriter + BinaryReader
763781
// ===========================================================================

modules/xml-serialization/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,5 @@ sharp_runtime_register_module(
55
NAME Xml.Serialization
66
TARGET sharp_runtime_xml_serialization
77
TYPE INTERFACE
8-
PUBLIC_DEPENDENCIES Collections.Core Xml
8+
PUBLIC_DEPENDENCIES Collections.Core IO Xml
99
)

modules/xml-serialization/include/System/Xml/Serialization/XmlSerializer.hpp

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <vector>
1010

1111
#include "System/Collections/Generic/List.hpp"
12+
#include "System/IO/StreamReader.hpp"
1213
#include "System/Xml/Serialization/detail/XmlLeafConvert.hpp"
1314
#include "System/Xml/Serialization/detail/XmlMember.hpp"
1415
#include "System/Xml/XmlDocument.hpp"
@@ -101,6 +102,19 @@ namespace System::Xml::Serialization {
101102
return result;
102103
}
103104

105+
/**
106+
* @brief Deserializes an XML document read from the stream's current position.
107+
*
108+
* @param stream Readable stream containing the XML document.
109+
* @return Deserialized value.
110+
* @throws System::ArgumentException if the stream is not readable.
111+
* @throws System::Xml::XmlException if the XML is malformed.
112+
*/
113+
[[nodiscard]] T Deserialize(System::IO::Stream& stream) const {
114+
System::IO::StreamReader reader(&stream, true);
115+
return Deserialize(reader.ReadToEnd());
116+
}
117+
104118
/**
105119
* @brief Appends @p value to @p parent as one element, inside a document the caller owns.
106120
*

modules/xml-serialization/tests/System/Xml/Serialization/XmlSerializerTests.cpp

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include <gtest/gtest.h>
1616

1717
#include "System/Xml/Serialization/XmlSerializer.hpp"
18+
#include "System/IO/MemoryStream.hpp"
1819

1920
using System::Xml::Serialization::XmlSerializationOptions;
2021
using System::Xml::Serialization::XmlSerializer;
@@ -35,6 +36,23 @@ struct SaveGameDescriptionData {
3536
SHARP_XML_M(SaveGameDescriptionData, Description))
3637
};
3738

39+
TEST(XmlSerializerStreamTests, DeserializeReadsFromCurrentStreamPositionAndLeavesStreamOpen) {
40+
const std::string xml = "prefix<?xml version=\"1.0\"?><SaveGameDescription>"
41+
"<FileName>slot1</FileName><ChapterName>One</ChapterName>"
42+
"<Description>Ready</Description></SaveGameDescription>";
43+
System::IO::MemoryStream stream(
44+
reinterpret_cast<const SharpRuntime::bytecs*>(xml.data()),
45+
static_cast<SharpRuntime::intcs>(xml.size()), false);
46+
stream.setPositionProperty(6);
47+
48+
const SaveGameDescriptionData value = XmlSerializer<SaveGameDescriptionData>{}.Deserialize(stream);
49+
50+
EXPECT_EQ(value.FileName, "slot1");
51+
EXPECT_EQ(value.ChapterName, "One");
52+
EXPECT_EQ(value.Description, "Ready");
53+
EXPECT_TRUE(stream.getCanReadProperty());
54+
}
55+
3856
// --- ShipGame's Entity/EntityList: ShipGame/ShipGame/EntityList.cs ----------------------------
3957

4058
struct MatrixData {

0 commit comments

Comments
 (0)