Skip to content
Draft
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
2 changes: 2 additions & 0 deletions src/Inferno.Core/Inferno.Core.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@
<ClInclude Include="Types.h" />
<ClInclude Include="Utility.h" />
<ClInclude Include="Wall.h" />
<ClInclude Include="walls_container.h" />
<ClInclude Include="Weapon.h" />
</ItemGroup>
<ItemGroup>
Expand All @@ -151,6 +152,7 @@
<ClCompile Include="Polymodel.cpp" />
<ClCompile Include="Segment.cpp" />
<ClCompile Include="Sound.cpp" />
<ClCompile Include="walls_container.cpp" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
Expand Down
6 changes: 6 additions & 0 deletions src/Inferno.Core/Inferno.Core.vcxproj.filters
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,9 @@
<ClInclude Include="OutrageRoom.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="walls_container.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
Expand Down Expand Up @@ -142,5 +145,8 @@
<ClCompile Include="OutrageRoom.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="walls_container.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
</Project>
103 changes: 58 additions & 45 deletions src/Inferno.Core/Level.h
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
#include "Wall.h"
#include "DataPool.h"
#include "Segment.h"
#include "walls_container.h"

namespace Inferno {
struct Matcen {
Expand Down Expand Up @@ -86,7 +87,7 @@ namespace Inferno {
int Segments; // Note that source ports allow thousands of segments
int Matcens = 20;
int Vertices;
int Walls;
size_t Walls;
int WallSwitches = 50;
int WallLinks = 100;
int FuelCenters = 70;
Expand All @@ -103,6 +104,17 @@ namespace Inferno {
constexpr auto MaxLightDeltas = 32000; // Rebirth limit. Original D2: 10000

struct Level {
// 1: Descent 1
// 2 to 7: Descent 2
// 8: Vertigo Enhanced
// >8: D2X-XL, unsupported
int Version;
// 22 to 25: Descent 1
// 26 to 29: Descent 2
// >32: D2X-XL, unsupported
int16 GameVersion;

LevelLimits Limits;
string Palette = "groupa.256";
SegID SecretExitReturn = SegID(0);
Matrix3x3 SecretReturnOrientation;
Expand All @@ -111,7 +123,7 @@ namespace Inferno {
List<Segment> Segments;
List<string> Pofs;
List<Object> Objects;
List<Wall> Walls;
WallsContainer Walls;
List<Trigger> Triggers;
List<Matcen> Matcens;
List<FlickeringLight> FlickeringLights; // Vertigo flickering lights
Expand All @@ -128,17 +140,7 @@ namespace Inferno {
List<LightDeltaIndex> LightDeltaIndices; // Index into LightDeltas
List<LightDelta> LightDeltas; // For breakable or flickering lights

// 22 to 25: Descent 1
// 26 to 29: Descent 2
// >32: D2X-XL, unsupported
int16 GameVersion{};

// 1: Descent 1
// 2 to 7: Descent 2
// 8: Vertigo Enhanced
// >8: D2X-XL, unsupported
int Version{};
LevelLimits Limits = { 1 };

DataPool<ActiveDoor> ActiveDoors{ ActiveDoor::IsAlive, 20 };

Expand All @@ -154,6 +156,13 @@ namespace Inferno {
Vector3 CameraUp;
#pragma endregion

Level(int version, WallsSerialization serialization)
: Version{ version }
, GameVersion{ version == 1 ? 25 : 32 }
, Limits{ Version }
, Walls{ Limits.Walls, serialization }
{ }

bool IsDescent1() const { return Version == 1; }
// Includes vertigo and non-vertigo
bool IsDescent2() const { return Version > 1 && Version <= 8; }
Expand Down Expand Up @@ -205,16 +214,12 @@ namespace Inferno {
return count;
}

constexpr const Wall& GetWall(WallID id) const { return Walls[(int)id]; }
constexpr Wall& GetWall(WallID id) { return Walls[(int)id]; }

constexpr Wall* TryGetWall(Tag tag) {
if (tag.Segment == SegID::None) return nullptr;

if (auto seg = TryGetSegment(tag)) {
auto id = (int)seg->GetSide(tag.Side).Wall;
if (Seq::inRange(Walls, id))
return &Walls[id];
auto id = seg->GetSide(tag.Side).Wall;
return Walls.TryGetWall(id);
}

return nullptr;
Expand All @@ -228,29 +233,6 @@ namespace Inferno {
return WallID::None;
}

constexpr Wall* TryGetWall(TriggerID trigger) {
if (trigger == TriggerID::None) return nullptr;

for (auto& wall : Walls) {
if (wall.Trigger == trigger)
return &wall;
}

return nullptr;
}


const Wall* TryGetWall(WallID id) const {
if (id == WallID::None || (int)id >= Walls.size())
return nullptr;

// Check for invalid walls
if (Walls[(int)id].Tag.Segment == SegID::None) return nullptr;
return &Walls[(int)id];
}

Wall* TryGetWall(WallID id) { return (Wall*)std::as_const(*this).TryGetWall(id); }

// Tries to get the side connecting the two segments
SideID GetConnectedSide(SegID src, SegID dst) const {
if (!SegmentExists(src) || !SegmentExists(dst))
Expand Down Expand Up @@ -289,7 +271,7 @@ namespace Inferno {

// Gets the wall connected to the other side of a wall (if present)
WallID GetConnectedWall(WallID wallId) {
auto wall = TryGetWall(wallId);
auto wall = Walls.TryGetWall(wallId);
if (!wall) return WallID::None;
auto other = GetConnectedSide(wall->Tag);
return TryGetWallID(other);
Expand Down Expand Up @@ -371,7 +353,7 @@ namespace Inferno {
}

TriggerID GetTriggerID(WallID wid) const {
auto wall = TryGetWall(wid);
auto wall = Walls.TryGetWall(wid);
if (!wall) return TriggerID::None;
return wall->Trigger;
}
Expand All @@ -383,7 +365,7 @@ namespace Inferno {
}

Trigger* TryGetTrigger(WallID wid) {
auto wall = TryGetWall(wid);
auto wall = Walls.TryGetWall(wid);
if (!wall) return nullptr;
return TryGetTrigger(wall->Trigger);
}
Expand Down Expand Up @@ -444,6 +426,37 @@ namespace Inferno {
bool CanAddMatcen() { return Matcens.size() < Limits.Matcens; }

size_t Serialize(StreamWriter& writer);
static Level Deserialize(span<ubyte>);
static Level Deserialize(span<ubyte>, WallsSerialization);

//creates closed walls from a single shared one
//to be called from the LevelReader
size_t CreateClosed(std::unordered_map<WallID, std::vector<Tag>> const& wallIdRefs) {
//check consistency
auto closedId = WallID::None;
for (auto&& [id, tags] : wallIdRefs) {
if (tags.size() == 1)
continue;
if (closedId != WallID::None)
throw Exception("CreateClosed: bad map, multiple shared walls");
assert(tags.size() > 1);
Wall* wall = Walls.TryGetWall(id);
if (!wall || !wall->IsSimplyClosed())
throw Exception("CreateClosed: bad map, shared wall is not closed with no trigger");
closedId = id;
//don't break here, we want to check the whole map for consistency
}
if (closedId == WallID::None)
return 0;

auto&& v = wallIdRefs.at(closedId);
for (auto it = v.begin() + 1; it != v.end(); ++it) { //skip the first tag
Wall w;
w.Type = WallType::Closed;
w.Tag = *it;
auto id = Walls.Append(std::move(w));
GetSide(*it).Wall = id; //the side is guaranteed to exist by the way the wallIdRefs was filled
}
return v.size() - 1;
}
};
}
71 changes: 59 additions & 12 deletions src/Inferno.Core/LevelReader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
#include "Streams.h"
#include "Utility.h"
#include "Pig.h"
#include "spdlog/spdlog.h"

namespace Inferno {
void ReadLevelInfo(StreamReader& reader, Level& level) {
Expand Down Expand Up @@ -42,10 +43,18 @@ namespace Inferno {

GameDataHeader _deltaLights{}, _deltaLightIndices{};

// This map is filled while reading sides and maps wallId to all
// sides (Tags) that reference it.
// Normally this is one to one relation. Now we (may)
// allow all closed walls without a trigger to be one wall
// referenced by many sides to save WallID's that are limited by 255.
// While reading such a file we need to "unpack" shared walls.
std::unordered_map<WallID, std::vector<Tag>> wallToTag_;

public:
LevelReader(span<ubyte> data) : _reader(data) {}

Level Read() {
Level Read(WallsSerialization wallsSerialization) {
auto sig = (uint)_reader.ReadInt32();
if (sig != MakeFourCC("LVLP"))
throw Exception("File is not a level (bad header)");
Expand All @@ -72,9 +81,8 @@ namespace Inferno {
_reader.ReadInt32();
}

Level level;
level.Version = _levelVersion;
level.Limits = LevelLimits(_levelVersion);
Level level{ _levelVersion, wallsSerialization };

ReadLevelInfo(_reader, level);
ReadSegments(level);
ReadGameData(level);
Expand Down Expand Up @@ -150,14 +158,17 @@ namespace Inferno {
seg.Connections[bit] = bitMask & (1 << bit) ? (SegID)_reader.ReadInt16() : SegID::None;
}

void ReadSegmentWalls(Segment& seg) {
void ReadSegmentWalls(Segment& seg, SegID id) {
auto mask = _reader.ReadByte();

for (int i = 0; i < MAX_SIDES; i++) {
auto& side = seg.Sides[i];

if (mask & (1 << i))
if (mask & (1 << i)) {
side.Wall = WallID(_reader.ReadByte());
////see the comment for wallToTag_
wallToTag_[side.Wall].emplace_back(id, static_cast<SideID>(i));
}
}
}

Expand All @@ -176,6 +187,7 @@ namespace Inferno {
for (auto& v : level.Vertices)
v = _reader.ReadVector();

size_t segmentId = 0;
for (auto& seg : level.Segments) {
auto bitMask = _reader.ReadByte();
bool hasSpecialData = bitMask & (1 << MAX_SIDES);
Expand All @@ -200,8 +212,9 @@ namespace Inferno {
seg.VolumeLight = Color(light, light, light);
}

ReadSegmentWalls(seg);
ReadSegmentWalls(seg, static_cast<SegID>(segmentId));
ReadSegmentTextures(seg);
++segmentId;
}

// D2 retail location for segment special data
Expand Down Expand Up @@ -490,7 +503,7 @@ namespace Inferno {
auto reactorTriggers = ReadHeader();
auto matcens = ReadHeader();

level.Walls.resize(walls.Count);
//level.Walls.resize(walls.Count);
level.Triggers.resize(triggers.Count);
level.Objects.resize(objects.Count);
level.Matcens.resize(matcens.Count);
Expand Down Expand Up @@ -520,8 +533,20 @@ namespace Inferno {
// Walls
if (walls.Offset != -1) {
_reader.Seek(walls.Offset);
for (auto& wall : level.Walls)
wall = ReadWall();
for (size_t i=0; i<walls.Count; ++i)
level.Walls.Append(ReadWall());
try {
//see the comment for wallToTag_
if (auto n = level.CreateClosed(wallToTag_)) {
SPDLOG_INFO(std::string("Found shared walls in the file, ") + std::to_string(n) + " re-created");
if (level.Walls.Overfilled())
throw Exception("The file contains too many walls, try activating shared closed walls option");
}
}
catch (Exception const& e) {
SPDLOG_ERROR(e.what());
throw;
}
}

if (triggers.Offset != -1) {
Expand All @@ -530,6 +555,28 @@ namespace Inferno {
t = ReadTrigger();
}

// temporary code to repair the trigger id bug:
// TriggerID::None (255) has been decremented when removing a trigger
// leading to many walls having a non-none controlling trigger
// that does not exist. If deletion is repeated many times the non-existing
// trigger ids could even become existing ids potentially causing
// unexpected level behavior
for (auto& w : level.Walls) {
if ((int)w.ControllingTrigger >= level.Triggers.size())
w.ControllingTrigger = TriggerID::None;
}
//just in case: check all trigger targets
size_t counter = 0;
for (auto& t : level.Triggers) {
for (auto& tar : t.Targets) {
if (auto w = level.TryGetWall(tar))
w->ControllingTrigger = (TriggerID)counter;
}
++counter;
}
//end of temporary repair


// Control center triggers
if (reactorTriggers.Offset != -1) {
_reader.Seek(reactorTriggers.Offset);
Expand All @@ -542,8 +589,8 @@ namespace Inferno {
}
};

Level Level::Deserialize(span<ubyte> data) {
Level Level::Deserialize(span<ubyte> data, WallsSerialization serialization) {
LevelReader reader(data);
return reader.Read();
return reader.Read(serialization);
}
}
Loading