feat: read and keep a scene file's sounds and particles as the client does

Files from before chunks now read their sounds and particles after the
objects, and chunked files their particle chunk:
- sounds (SceneLoader::LoadSounds, version 5 on, only before chunks):
  position and config (before 35 a 260 character name, kept as
  soundName);
- particles (SceneLoader::LoadParticles, version 5 on): priority (43
  on), position, rotation, effect names and config (46 on; before, an
  unused u16); the client stops at one with no effect, and before 44 a
  file may end before them.

Every scene file of every client on disk reads each part to the next
part's start (up to the client's 16 byte padding): 5408 referenced by
zone files and 5125 on disk, versions 31-43 before chunks and 44-49
chunked. One client file, a LUP world, ends before its sounds; its
objects are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Aaron Kimbrell
2026-10-27 10:34:46 -05:00
committed by GitHub
parent e3370d7042
commit 066571ea28
3 changed files with 101 additions and 2 deletions

View File

@@ -243,6 +243,8 @@ void Level::ReadChunks(std::istream& file) {
if (fileInfo.fileInfo.particleChunkStart != 0) {
const auto header = ReadChunkHeader(file, fileInfo.fileInfo.particleChunkStart);
m_ChunkHeaders.insert({ header.id, header });
file.seekg(header.startPosition);
ReadParticles(file);
}
return;
}
@@ -270,6 +272,8 @@ void Level::ReadChunks(std::istream& file) {
ReadSkydomeInfo(file);
ReadEditorSettings(file);
ReadSceneObjectDataChunk(file);
ReadSounds(file);
ReadParticles(file);
}
Level::Header Level::ReadChunkHeader(std::istream& file, uint32_t start) {
@@ -419,3 +423,52 @@ void Level::ReadSceneObjectDataChunk(std::istream& file) {
m_Objects.push_back(std::move(obj));
}
}
void Level::ReadSounds(std::istream& file) {
// SceneLoader::LoadSounds (0x0103c110), only in files from before chunks
if (m_Version < 5) return;
uint32_t count = 0;
BinaryIO::BinaryRead(file, count);
for (uint32_t i = 0; i < count; ++i) {
LevelSound sound;
BinaryIO::BinaryRead(file, sound.position);
if (m_Version < 35) {
// A 260 character name, up to its first null
std::u16string name(260, u'\0');
file.read(reinterpret_cast<char*>(name.data()), static_cast<std::streamsize>(name.size() * sizeof(char16_t)));
name.resize(std::min(name.find(u'\0'), name.size()));
sound.config.Insert<std::u16string>(u"soundName", name);
} else {
for (const auto& token : GeneralUtils::SplitString(ReadWideString32(file), '\n')) sound.config.ParseInsert(token);
}
if (file.fail()) throw std::runtime_error("Failed to read from istream.");
m_Sounds.push_back(std::move(sound));
}
}
void Level::ReadParticles(std::istream& file) {
// SceneLoader::LoadParticles (0x01048320); before version 44 a file may end before them
if (m_Version < 5) return;
if (m_Version < 44 && file.peek() == std::char_traits<char>::eof()) {
file.clear();
return;
}
uint32_t count = 0;
BinaryIO::BinaryRead(file, count);
std::string config;
for (uint32_t i = 0; i < count; ++i) {
LevelParticle particle;
if (m_Version >= 43) BinaryIO::BinaryRead(file, particle.priority);
BinaryIO::BinaryRead(file, particle.position);
BinaryIO::BinaryRead(file, particle.rotation);
particle.effectNames = ReadWideString32(file);
if (m_Version < 46) BinaryIO::BinaryRead(file, particle.unknown);
else config = ReadWideString32(file);
particle.config = config;
if (file.fail()) throw std::runtime_error("Failed to read from istream.");
// The client stops at a particle with no effect
if (particle.effectNames.empty()) break;
m_Particles.push_back(std::move(particle));
}
}