diablo2-web/tools/d2moo-oracle/src/main.cpp

817 lines
28 KiB
C++

// D2MOO DRLG oracle driver: generates Act I levels with the native D2MOO code and dumps every
// stage the TypeScript port is diffed against (schema v3). See ../README.md.
//
// Usage:
// d2moo-oracle --data <dir> --seed <n> --levels 2,3,4 [--difficulty 0] [--isolated]
// [--trace <file>] --out <file.json>
#include "oracle.h"
#include "Windows.h"
#include "D2DataTbls.h"
#include "Drlg/D2DrlgDrlg.h"
#include "Drlg/D2DrlgDrlgGrid.h"
#include "Drlg/D2DrlgDrlgLogic.h"
#include "Drlg/D2DrlgDrlgRoom.h"
#include "Drlg/D2DrlgDrlgVer.h"
#include "Drlg/D2DrlgOutdoors.h"
#include "Drlg/D2DrlgOutRoom.h"
#include "Drlg/D2DrlgPreset.h"
#include "Drlg/D2DrlgRoomTile.h"
#include "D2Seed.h"
#include <DataTbls/LevelsIds.h>
#include <cstdint>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <malloc.h>
#include <map>
#include <sstream>
#include <string>
#include <vector>
#ifndef D2ORACLE_D2MOO_COMMIT
#error "build.sh must define D2ORACLE_D2MOO_COMMIT"
#endif
namespace
{
std::string Seed(const D2SeedStrc& s)
{
return "[" + std::to_string(s.nLowSeed) + "," + std::to_string(s.nHighSeed) + "]";
}
std::string Coord(const D2DrlgCoordStrc& c)
{
std::ostringstream o;
o << "[" << c.nPosX << "," << c.nPosY << "," << c.nWidth << "," << c.nHeight << "]";
return o.str();
}
// {"w":W,"h":H,"cells":[row-major int32]} or null when the grid was never allocated.
std::string Grid(const D2DrlgGridStrc& grid)
{
if (!grid.pCellsFlags || !grid.pCellsRowOffsets)
{
return "null";
}
std::ostringstream o;
o << "{\"w\":" << grid.nWidth << ",\"h\":" << grid.nHeight << ",\"cells\":[";
for (int y = 0; y < grid.nHeight; ++y)
{
for (int x = 0; x < grid.nWidth; ++x)
{
if (x || y) o << ",";
o << grid.pCellsFlags[x + grid.pCellsRowOffsets[y]];
}
}
o << "]}";
return o.str();
}
std::string PresetUnits(const D2PresetUnitStrc* pUnit)
{
std::ostringstream o;
o << "[";
for (bool first = true; pUnit; pUnit = pUnit->pNext, first = false)
{
if (!first) o << ",";
o << "[" << pUnit->nUnitType << "," << pUnit->nIndex << "," << pUnit->nMode << "," << pUnit->nXpos << "," << pUnit->nYpos << "," << pUnit->bSpawned << "]";
}
o << "]";
return o.str();
}
std::string Orths(const D2DrlgOrthStrc* pOrth)
{
std::ostringstream o;
o << "[";
for (bool first = true; pOrth; pOrth = pOrth->pNext, first = false)
{
if (!first) o << ",";
// Level-to-level orths store the neighbour level in the union.
o << "{\"level\":" << (pOrth->pLevel ? pOrth->pLevel->nLevelId : -1) << ",\"dir\":" << (int)pOrth->nDirection
<< ",\"preset\":" << pOrth->bPreset << ",\"init\":" << pOrth->bInit
<< ",\"box\":" << (pOrth->pBox ? Coord(*pOrth->pBox) : std::string("null")) << "}";
}
o << "]";
return o.str();
}
const char* PrestFile(const D2DrlgMapStrc* pMap)
{
D2LvlPrestTxt* pPrest = DATATBLS_GetLvlPrestTxtRecord(pMap->nLevelPrest);
if (!pPrest || pMap->nPickedFile < 0 || pMap->nPickedFile >= 6)
{
return "";
}
return pPrest->szFile[pMap->nPickedFile];
}
std::string Map(const D2DrlgMapStrc* pMap)
{
std::ostringstream o;
o << "{\"prest\":" << pMap->nLevelPrest << ",\"picked\":" << pMap->nPickedFile << ",\"coord\":" << Coord(pMap->pDrlgCoord)
<< ",\"file\":\"";
for (const char* p = PrestFile(pMap); *p; ++p)
{
o << (*p == '\\' ? '/' : *p);
}
o << "\",\"hasInfo\":" << pMap->bHasInfo << ",\"units\":" << PresetUnits(pMap->pPresetUnit) << "}";
return o.str();
}
// ---- act stage ----
std::string ActLevel(D2DrlgLevelStrc* pLevel)
{
std::ostringstream o;
o << "{\"id\":" << pLevel->nLevelId << ",\"drlgType\":" << pLevel->nDrlgType << ",\"levelType\":" << pLevel->nLevelType
<< ",\"coord\":" << Coord(pLevel->pLevelCoords) << ",\"flags\":" << pLevel->dwFlags << ",\"seed\":" << Seed(pLevel->pSeed);
if (pLevel->nDrlgType == DRLGTYPE_OUTDOOR && pLevel->pOutdoors)
{
o << ",\"outdoor\":{\"flags\":" << pLevel->pOutdoors->dwFlags << ",\"roomData\":" << Orths(pLevel->pOutdoors->pRoomData) << "}";
}
if (pLevel->nDrlgType == DRLGTYPE_PRESET && pLevel->pPreset)
{
o << ",\"preset\":{\"direction\":" << pLevel->pPreset->nDirection << ",\"map\":"
<< (pLevel->pPreset->pDrlgMap ? Map(pLevel->pPreset->pDrlgMap) : std::string("null")) << "}";
}
o << "}";
return o.str();
}
std::string Act(D2DrlgStrc* pDrlg)
{
std::map<int, D2DrlgLevelStrc*> levels;
for (D2DrlgLevelStrc* pLevel = pDrlg->pLevel; pLevel; pLevel = pLevel->pNextLevel)
{
levels[pLevel->nLevelId] = pLevel;
}
std::ostringstream o;
o << "[";
bool first = true;
for (auto& [id, pLevel] : levels)
{
if (!first) o << ",";
first = false;
o << ActLevel(pLevel);
}
o << "]";
return o.str();
}
// ---- levelGrid stage (after DRLGOUTWILD_InitAct1OutdoorLevel, before any room exists) ----
// Vertex references: -1 = null, 0..23 = pOutdoors->pVertices[i], 100+k = k-th vertex of the pVertex ring.
std::map<int, std::string> gLevelGridJson;
std::string LevelGrid(D2DrlgLevelStrc* pLevel)
{
D2DrlgOutdoorInfoStrc* pOut = pLevel->pOutdoors;
std::vector<D2DrlgVertexStrc*> ring;
if (D2DrlgVertexStrc* pStart = pOut->pVertex)
{
D2DrlgVertexStrc* p = pStart;
do
{
ring.push_back(p);
p = p->pNext;
} while (p && p != pStart && ring.size() < 4096);
}
auto ref = [&](const D2DrlgVertexStrc* p) -> int {
if (!p) return -1;
if (p >= &pOut->pVertices[0] && p < &pOut->pVertices[24]) return (int)(p - &pOut->pVertices[0]);
for (size_t k = 0; k < ring.size(); ++k)
{
if (ring[k] == p) return 100 + (int)k;
}
D2ORACLE_Fail("level %d: vertex pointer outside pVertices and the ring", pLevel->nLevelId);
};
auto vertex = [&](const D2DrlgVertexStrc& v) {
std::ostringstream o;
o << "[" << v.nPosX << "," << v.nPosY << "," << (int)v.nDirection << "," << v.dwFlags << "," << ref(v.pNext) << "]";
return o.str();
};
std::ostringstream o;
o << "{\"flags\":" << pOut->dwFlags << ",\"coord\":" << Coord(pOut->pCoord) << ",\"grids\":[";
for (int i = 0; i < 4; ++i)
{
if (i) o << ",";
o << Grid(pOut->pGrid[i]);
}
o << "],\"ring\":[";
for (size_t k = 0; k < ring.size(); ++k)
{
if (k) o << ",";
o << vertex(*ring[k]);
}
o << "],\"nVertices\":" << pOut->nVertices << ",\"vertices\":[";
for (int i = 0; i < 24; ++i)
{
if (i) o << ",";
o << vertex(pOut->pVertices[i]);
}
o << "],\"pathStarts\":[";
// sub_6FD80750 (DrlgOutPlace.cpp:470-505) builds each path as a null-terminated chain of
// pool-allocated vertices, so the chains are dumped by value.
for (int i = 0; i < 6; ++i)
{
if (i) o << ",";
if (!pOut->pPathStarts[i])
{
o << "null";
continue;
}
o << "[";
int n = 0;
for (const D2DrlgVertexStrc* p = pOut->pPathStarts[i]; p; p = p->pNext, ++n)
{
if (n >= 4096) D2ORACLE_Fail("level %d: path %d does not terminate", pLevel->nLevelId, i);
o << (n ? "," : "") << "[" << p->nPosX << "," << p->nPosY << "," << (int)p->nDirection << "," << p->dwFlags << "]";
}
o << "]";
}
o << "],\"roomData\":" << Orths(pOut->pRoomData) << ",\"seed\":" << Seed(pLevel->pSeed) << "}";
return o.str();
}
} // namespace
extern "C" void __real__Z32DRLGOUTWILD_InitAct1OutdoorLevelP15D2DrlgLevelStrc(D2DrlgLevelStrc* pLevel);
// Linked with -Wl,--wrap: DRLGOUTDOORS_GenerateLevel's call to DRLGOUTWILD_InitAct1OutdoorLevel lands
// here, so the level grids are captured exactly between InitAct1OutdoorLevel and room creation.
extern "C" void __wrap__Z32DRLGOUTWILD_InitAct1OutdoorLevelP15D2DrlgLevelStrc(D2DrlgLevelStrc* pLevel)
{
__real__Z32DRLGOUTWILD_InitAct1OutdoorLevelP15D2DrlgLevelStrc(pLevel);
gLevelGridJson[pLevel->nLevelId] = LevelGrid(pLevel);
}
namespace
{
std::string Rooms(D2DrlgLevelStrc* pLevel, std::vector<D2DrlgMapStrc*>& maps)
{
auto mapIndex = [&](D2DrlgMapStrc* pMap) -> int {
for (size_t i = 0; i < maps.size(); ++i)
{
if (maps[i] == pMap) return (int)i;
}
maps.push_back(pMap);
return (int)maps.size() - 1;
};
std::ostringstream o;
o << "[";
int i = 0;
for (D2DrlgRoomStrc* pRoom = pLevel->pFirstRoomEx; pRoom; pRoom = pRoom->pDrlgRoomNext, ++i)
{
if (i) o << ",";
o << "{\"type\":" << pRoom->nType << ",\"coord\":" << Coord(pRoom->pDrlgCoord) << ",\"flags\":" << pRoom->dwFlags
<< ",\"otherFlags\":" << pRoom->dwOtherFlags << ",\"dt1Mask\":" << pRoom->dwDT1Mask << ",\"initSeed\":" << pRoom->dwInitSeed
<< ",\"seed\":" << Seed(pRoom->pSeed);
if (pRoom->nType == DRLGTYPE_MAZE)
{
if (!pRoom->pOutdoor) D2ORACLE_Fail("level %d room %d: maze room without pOutdoor", pLevel->nLevelId, i);
const D2DrlgOutdoorRoomStrc* p = pRoom->pOutdoor;
o << ",\"outdoor\":{\"flags\":" << p->dwFlags << ",\"flagsEx\":" << p->dwFlagsEx << ",\"subType\":" << p->nSubType
<< ",\"subTheme\":" << p->nSubTheme << ",\"subThemePicked\":" << p->nSubThemePicked << "}";
}
else if (pRoom->nType == DRLGTYPE_PRESET)
{
if (!pRoom->pMaze || !pRoom->pMaze->pMap) D2ORACLE_Fail("level %d room %d: preset room without map", pLevel->nLevelId, i);
const D2DrlgPresetRoomStrc* p = pRoom->pMaze;
o << ",\"preset\":{\"prest\":" << p->nLevelPrest << ",\"picked\":" << p->nPickedFile << ",\"flags\":" << p->dwFlags
<< ",\"map\":" << mapIndex(p->pMap) << "}";
}
else
{
D2ORACLE_Fail("level %d room %d: unexpected room type %d", pLevel->nLevelId, i, pRoom->nType);
}
o << "}";
}
o << "]";
return o.str();
}
// ---- activation stage ----
struct RoomActivation
{
D2DrlgRoomStrc* pRoom;
// Schema-2 fields, captured right after DRLGROOMTILE_InitRoomGrids (JSON object without the closing brace).
std::string szHead;
// Room seed right after the room's own DRLGROOMTILE_AddRoomMapTiles.
std::string szSeedAfterTiles;
};
// Mirrors DRLGACTIVATE_InitializeRoomEx (DrlgActivate.cpp:317-331) followed by
// DRLGACTIVATE_RoomEx_EnsureHasRoom (DrlgActivate.cpp:95-115) up to DRLGROOMTILE_AddRoomMapTiles.
// DRLG_CreateRoomForRoomEx (the D2ActiveRoomStrc, built by D2Game) is not run, so pRoom stays NULL for
// every room and D2Common_COLLISION_FirstFn_6FD41000 is a no-op (D2Collision.cpp: returns when pRoom is
// NULL). In the game a room activated earlier already has its active room, so the linked-tile merge of
// a later room also updates that room's collision grid (no effect on the DRLG tile data or the RNG).
// Rooms are activated in pFirstRoomEx order; a preset map spanning several rooms loads its DS1 with
// the seed of the first activated room, which only matters for the randomly skipped preset units
// of DRLGPRESET_AddPresetUnitToDrlgMap (none of them occur in Act I outdoor DS1s). The activation
// order also decides the room-seam merge of DRLGROOMTILE_GetCreateLinkedTileData: the room activated
// later edits the tiles of the near rooms activated before it.
std::vector<RoomActivation> Activate(D2DrlgLevelStrc* pLevel)
{
std::vector<RoomActivation> out;
int i = 0;
char szContext[64];
for (D2DrlgRoomStrc* pRoom = pLevel->pFirstRoomEx; pRoom; pRoom = pRoom->pDrlgRoomNext, ++i)
{
snprintf(szContext, sizeof(szContext), "L%d/a%d", pLevel->nLevelId, i);
D2ORACLE_SetTraceContext(szContext);
if (!(pRoom->dwFlags & DRLGROOMFLAG_TILELIB_LOADED))
{
DRLGROOMTILE_LoadDT1FilesForRoom(pRoom);
}
if (!(pRoom->dwFlags & DRLGROOMFLAG_PRESET_UNITS_ADDED) && pRoom->nType == DRLGTYPE_PRESET)
{
DRLGPRESET_SpawnHardcodedPresetUnits(pRoom);
}
if (pRoom->pRoom || (pRoom->dwFlags & DRLGROOMFLAG_HAS_ROOM))
{
D2ORACLE_Fail("level %d room %d: already has an active room", pLevel->nLevelId, i);
}
if (!pRoom->nRoomsNear)
{
sub_6FD77BB0(pLevel->pDrlg->pMempool, pRoom);
}
DRLGROOMTILE_InitRoomGrids(pRoom);
std::ostringstream o;
o << "{\"seed\":" << Seed(pRoom->pSeed);
if (pRoom->nType == DRLGTYPE_MAZE)
{
const D2DrlgOutdoorRoomStrc* p = pRoom->pOutdoor;
o << ",\"tile\":" << Grid(p->pTileTypeGrid) << ",\"wall\":" << Grid(p->pWallGrid) << ",\"floor\":" << Grid(p->pFloorGrid)
<< ",\"dirt\":" << Grid(p->pDirtPathGrid);
}
else
{
const D2DrlgPresetRoomStrc* p = pRoom->pMaze;
const D2DrlgFileStrc* pFile = p->pMap->pFile;
o << ",\"walls\":[";
for (int l = 0; l < pFile->nWallLayers; ++l) o << (l ? "," : "") << Grid(p->pWallGrid[l]);
o << "],\"tiles\":[";
for (int l = 0; l < pFile->nWallLayers; ++l) o << (l ? "," : "") << Grid(p->pTileTypeGrid[l]);
o << "],\"floors\":[";
for (int l = 0; l < pFile->nFloorLayers; ++l) o << (l ? "," : "") << Grid(p->pFloorGrid[l]);
o << "],\"shadow\":" << Grid(p->pCellGrid);
}
o << ",\"units\":" << PresetUnits(pRoom->pPresetUnits);
DRLGROOMTILE_AddRoomMapTiles(pRoom);
out.push_back({pRoom, o.str(), Seed(pRoom->pSeed)});
}
return out;
}
// ---- mapTiles (schema 3): the room's D2DrlgTileGridStrc after every requested room is activated ----
// Pointers are dumped as references, resolved over every room of the act (pDrlg->pLevel chain, rooms
// numbered in pFirstRoomEx order), because warps and linked tiles cross level seams:
// D2DrlgTileDataStrc* -> [levelId, room, "w"|"f"|"r", index] into that room's pTileGrid->pTiles
// pWallTiles / pFloorTiles / pRoofTiles (the pool block, not only [0, count))
// D2RoomCoordListStrc* -> [levelId, room, "c", index in that room's pLogicalRoomInfo->pCoordList chain]
// D2DrlgRoomStrc* -> [levelId, room]
struct TileBlock
{
int nLevelId;
int nRoom;
char cArray;
size_t nBytes;
};
struct RefIndex
{
std::map<uintptr_t, TileBlock> tileBlocks;
std::map<const D2RoomCoordListStrc*, std::string> coordLists;
std::map<const D2DrlgRoomStrc*, std::string> rooms;
};
constexpr int kMaxChain = 1 << 20;
void AddTileBlock(RefIndex& index, const D2DrlgTileDataStrc* pTiles, int nLevelId, int nRoom, char cArray)
{
if (!pTiles)
{
return;
}
// FOG_AllocPool / FOG_ReallocPool are calloc / realloc, so a pool block is a malloc block.
const TileBlock block = {nLevelId, nRoom, cArray, malloc_usable_size((void*)pTiles)};
if (!index.tileBlocks.emplace((uintptr_t)pTiles, block).second)
{
D2ORACLE_Fail("level %d room %d: tile array %c starts at another array's address", nLevelId, nRoom, cArray);
}
}
RefIndex BuildRefIndex(D2DrlgStrc* pDrlg)
{
RefIndex index;
for (D2DrlgLevelStrc* pLevel = pDrlg->pLevel; pLevel; pLevel = pLevel->pNextLevel)
{
int nRoom = 0;
for (D2DrlgRoomStrc* pRoom = pLevel->pFirstRoomEx; pRoom; pRoom = pRoom->pDrlgRoomNext, ++nRoom)
{
const std::string szRoom = std::to_string(pLevel->nLevelId) + "," + std::to_string(nRoom);
index.rooms[pRoom] = "[" + szRoom + "]";
if (const D2DrlgTileGridStrc* pTileGrid = pRoom->pTileGrid)
{
AddTileBlock(index, pTileGrid->pTiles.pWallTiles, pLevel->nLevelId, nRoom, 'w');
AddTileBlock(index, pTileGrid->pTiles.pFloorTiles, pLevel->nLevelId, nRoom, 'f');
AddTileBlock(index, pTileGrid->pTiles.pRoofTiles, pLevel->nLevelId, nRoom, 'r');
}
if (const D2DrlgLogicalRoomInfoStrc* pInfo = pRoom->pLogicalRoomInfo)
{
int k = 0;
for (const D2RoomCoordListStrc* p = pInfo->pCoordList; p; p = p->pNext, ++k)
{
if (k >= kMaxChain) D2ORACLE_Fail("level %d room %d: pCoordList chain does not terminate", pLevel->nLevelId, nRoom);
index.coordLists[p] = "[" + szRoom + ",\"c\"," + std::to_string(k) + "]";
}
}
}
}
return index;
}
std::string TileRef(const RefIndex& index, const D2DrlgTileDataStrc* p)
{
if (!p)
{
return "null";
}
auto it = index.tileBlocks.upper_bound((uintptr_t)p);
if (it != index.tileBlocks.begin())
{
--it;
const uintptr_t nOffset = (uintptr_t)p - it->first;
if (nOffset < it->second.nBytes)
{
if (nOffset % sizeof(D2DrlgTileDataStrc))
{
D2ORACLE_Fail("tile pointer %p is not an element of level %d room %d array %c", (const void*)p, it->second.nLevelId, it->second.nRoom, it->second.cArray);
}
return "[" + std::to_string(it->second.nLevelId) + "," + std::to_string(it->second.nRoom) + ",\"" + it->second.cArray + "\","
+ std::to_string(nOffset / sizeof(D2DrlgTileDataStrc)) + "]";
}
}
D2ORACLE_Fail("tile pointer %p is outside every tile array of the act (dangling?)", (const void*)p);
}
// D2DrlgTileDataStrc::unk0x10 holds (int)D2RoomCoordListStrc* when DRLGLOGIC set it (-m32), else an int.
std::string TileUnk10(const RefIndex& index, int32_t nValue)
{
if (nValue)
{
auto it = index.coordLists.find((const D2RoomCoordListStrc*)(uintptr_t)(uint32_t)nValue);
if (it != index.coordLists.end())
{
return it->second;
}
}
return std::to_string(nValue);
}
std::string TileEntry(const D2TileLibraryEntryStrc* pTile)
{
if (!pTile)
{
return "null";
}
std::string szLibrary;
int nIndex = -1;
D2ORACLE_DescribeTile(pTile, &szLibrary, &nIndex);
if (szLibrary.find_first_of("\"\\") != std::string::npos)
{
D2ORACLE_Fail("tile library path %s needs JSON escaping", szLibrary.c_str());
}
return "[\"" + szLibrary + "\"," + std::to_string(nIndex) + "]";
}
// Every D2DrlgTileDataStrc field in struct order (D2DrlgDrlg.h:333).
std::string TileData(const RefIndex& index, const D2DrlgTileDataStrc& t)
{
std::ostringstream o;
o << "[" << t.nWidth << "," << t.nHeight << "," << t.nPosX << "," << t.nPosY << "," << TileUnk10(index, t.unk0x10) << "," << t.dwFlags
<< "," << TileEntry(t.pTile) << "," << t.nTileType << "," << TileRef(index, t.unk0x20) << "," << t.unk0x24 << "," << (int)t.nRed
<< "," << (int)t.nGreen << "," << (int)t.nBlue << "," << (int)t.nIntensity << "," << t.unk0x2C << "]";
return o.str();
}
std::string TileDataArray(const RefIndex& index, const D2DrlgTileDataStrc* pTiles, int nTiles, const char* szArray, int nLevelId, int nRoom)
{
if (nTiles < 0 || (nTiles > 0 && !pTiles))
{
D2ORACLE_Fail("level %d room %d: %d %s without an array", nLevelId, nRoom, nTiles, szArray);
}
if (nTiles > 0 && (size_t)nTiles * sizeof(D2DrlgTileDataStrc) > malloc_usable_size((void*)pTiles))
{
D2ORACLE_Fail("level %d room %d: %d %s exceed their pool block", nLevelId, nRoom, nTiles, szArray);
}
std::string s = "[";
for (int i = 0; i < nTiles; ++i)
{
if (i) s += ",";
s += TileData(index, pTiles[i]);
}
return s + "]";
}
std::string CoordList(const D2RoomCoordListStrc& c)
{
std::ostringstream o;
o << "[" << c.nIndex << "," << c.bNode << "," << c.bRoomActive << "," << Coord(c.pBox[0]) << "," << Coord(c.pBox[1]) << "]";
return o.str();
}
// {flags, nLists, indexX, indexY, lists} or null. pIndexY cells hold (int)D2RoomCoordListStrc*
// (DRLGLOGIC_AssignCoordListsForGrids): dumped as 1 + the index of that list in the room's own
// pCoordList chain, 0 for NULL.
std::string LogicalRoomInfo(const D2DrlgRoomStrc* pRoom, int nLevelId, int nRoom)
{
const D2DrlgLogicalRoomInfoStrc* pInfo = pRoom->pLogicalRoomInfo;
if (!pInfo)
{
return "null";
}
std::map<uint32_t, int> chain;
std::string lists = "[";
int k = 0;
for (const D2RoomCoordListStrc* p = pInfo->pCoordList; p; p = p->pNext, ++k)
{
if (k >= kMaxChain) D2ORACLE_Fail("level %d room %d: pCoordList chain does not terminate", nLevelId, nRoom);
chain[(uint32_t)(uintptr_t)p] = k;
lists += (k ? "," : "") + CoordList(*p);
}
lists += "]";
std::ostringstream o;
o << "{\"flags\":" << pInfo->dwFlags << ",\"nLists\":" << pInfo->nLists << ",\"indexX\":" << Grid(pInfo->pIndexX) << ",\"indexY\":";
const D2DrlgGridStrc& grid = pInfo->pIndexY;
if (!grid.pCellsFlags || !grid.pCellsRowOffsets)
{
o << "null";
}
else
{
o << "{\"w\":" << grid.nWidth << ",\"h\":" << grid.nHeight << ",\"cells\":[";
for (int y = 0; y < grid.nHeight; ++y)
{
for (int x = 0; x < grid.nWidth; ++x)
{
const uint32_t nCell = (uint32_t)grid.pCellsFlags[x + grid.pCellsRowOffsets[y]];
int nValue = 0;
if (nCell)
{
auto it = chain.find(nCell);
if (it == chain.end()) D2ORACLE_Fail("level %d room %d: pIndexY cell (%d, %d) is not a list of the room's chain", nLevelId, nRoom, x, y);
nValue = it->second + 1;
}
o << ((x || y) ? "," : "") << nValue;
}
}
o << "]}";
}
o << ",\"lists\":" << lists << "}";
return o.str();
}
// pRoomTiles chain: [[levelId, room] of pDrlgRoom, pLvlWarpTxtRecord index (-1 for NULL), bEnabled,
// unk0x0C, unk0x10].
std::string RoomTiles(const RefIndex& index, const D2DrlgRoomStrc* pRoom, int nLevelId, int nRoom)
{
std::string s = "[";
int k = 0;
for (const D2RoomTileStrc* p = pRoom->pRoomTiles; p; p = p->pNext, ++k)
{
if (k >= kMaxChain) D2ORACLE_Fail("level %d room %d: pRoomTiles chain does not terminate", nLevelId, nRoom);
std::string szRoom = "null";
if (p->pDrlgRoom)
{
auto it = index.rooms.find(p->pDrlgRoom);
if (it == index.rooms.end()) D2ORACLE_Fail("level %d room %d: warp %d leads to a room outside the act", nLevelId, nRoom, k);
szRoom = it->second;
}
int nWarp = -1;
if (p->pLvlWarpTxtRecord)
{
const ptrdiff_t n = p->pLvlWarpTxtRecord - sgptDataTables->pLvlWarpTxt;
if (n < 0 || n >= sgptDataTables->nLvlWarpTxtRecordCount) D2ORACLE_Fail("level %d room %d: warp %d has a record outside LvlWarp.txt", nLevelId, nRoom, k);
nWarp = (int)n;
}
s += (k ? "," : "");
s += "[" + szRoom + "," + std::to_string(nWarp) + "," + std::to_string(p->bEnabled) + "," + TileRef(index, p->unk0x0C) + "," + TileRef(index, p->unk0x10) + "]";
}
return s + "]";
}
std::string MapTiles(const RefIndex& index, const D2DrlgRoomStrc* pRoom, int nLevelId, int nRoom)
{
const D2DrlgTileGridStrc* pTileGrid = pRoom->pTileGrid;
if (!pTileGrid)
{
D2ORACLE_Fail("level %d room %d: no pTileGrid after DRLGROOMTILE_AddRoomMapTiles", nLevelId, nRoom);
}
if (pTileGrid->pAnimTiles)
{
D2ORACLE_Fail("level %d room %d: animated tiles (pAnimTiles) are not dumped", nLevelId, nRoom);
}
const D2DrlgRoomTilesStrc& t = pTileGrid->pTiles;
std::ostringstream o;
o << "{\"nWalls\":" << pTileGrid->nWalls << ",\"nFloors\":" << pTileGrid->nFloors << ",\"nShadows\":" << pTileGrid->nShadows
<< ",\"pTiles\":{\"nWalls\":" << t.nWalls << ",\"nFloors\":" << t.nFloors << ",\"nRoofs\":" << t.nRoofs << "}"
<< ",\"walls\":" << TileDataArray(index, t.pWallTiles, t.nWalls, "walls", nLevelId, nRoom)
<< ",\"floors\":" << TileDataArray(index, t.pFloorTiles, t.nFloors, "floors", nLevelId, nRoom)
<< ",\"roofs\":" << TileDataArray(index, t.pRoofTiles, t.nRoofs, "roofs", nLevelId, nRoom) << ",\"links\":[";
int k = 0;
for (const D2DrlgTileLinkStrc* p = pTileGrid->pMapLinks; p; p = p->pNext, ++k)
{
if (k >= kMaxChain) D2ORACLE_Fail("level %d room %d: pMapLinks chain does not terminate", nLevelId, nRoom);
o << (k ? "," : "") << "[" << p->bFloor << "," << TileRef(index, p->pMapTile) << "]";
}
o << "],\"warps\":" << RoomTiles(index, pRoom, nLevelId, nRoom) << ",\"units\":" << PresetUnits(pRoom->pPresetUnits)
<< ",\"logical\":" << LogicalRoomInfo(pRoom, nLevelId, nRoom) << "}";
return o.str();
}
// Generation stage of one level: DRLG_InitLevel and everything that exists before any room is
// activated. Returns the level's JSON object without the closing brace.
std::string InitLevelStage(D2DrlgStrc* pDrlg, int nLevelId, D2DrlgLevelStrc** ppLevel)
{
char szContext[64];
snprintf(szContext, sizeof(szContext), "L%d", nLevelId);
D2ORACLE_SetTraceContext(szContext);
D2DrlgLevelStrc* pLevel = DRLG_GetLevel(pDrlg, nLevelId);
if (pLevel->nDrlgType != DRLGTYPE_OUTDOOR || DRLG_GetActNoFromLevelId(nLevelId) != ACT_I)
{
D2ORACLE_Fail("level %d is not an Act I outdoor level", nLevelId);
}
if (pLevel->pFirstRoomEx)
{
D2ORACLE_Fail("level %d was already generated before its own generation stage", nLevelId);
}
DRLG_InitLevel(pLevel);
auto it = gLevelGridJson.find(nLevelId);
if (it == gLevelGridJson.end())
{
D2ORACLE_Fail("level %d: InitAct1OutdoorLevel hook did not fire", nLevelId);
}
std::vector<D2DrlgMapStrc*> maps;
const std::string rooms = Rooms(pLevel, maps);
std::ostringstream o;
o << "{\"id\":" << nLevelId << ",\"levelGrid\":" << it->second << ",\"nRooms\":" << pLevel->nRooms << ",\"rooms\":" << rooms
<< ",\"maps\":[";
for (size_t i = 0; i < maps.size(); ++i)
{
o << (i ? "," : "") << Map(maps[i]);
}
o << "],\"levelSeedAfterRooms\":" << Seed(pLevel->pSeed) << ",\"warp\":{\"n\":" << pLevel->nRoomCoords << ",\"xy\":[";
for (int i = 0; i < pLevel->nRoomCoords; ++i)
{
o << (i ? "," : "") << "[" << pLevel->nRoom_Center_Warp_X[i] << "," << pLevel->nRoom_Center_Warp_Y[i] << "]";
}
o << "]}";
*ppLevel = pLevel;
return o.str();
}
D2DrlgStrc* AllocAct1(D2DrlgActStrc* pAct, uint32_t nSeed, uint8_t nDifficulty)
{
memset(pAct, 0, sizeof(*pAct));
pAct->nAct = ACT_I;
D2ORACLE_SetTraceContext("drlg");
// D2Game allocates the act with the town level id; DRLG_AllocDrlg lays out the whole act
// (DRLGOUTPLACE_CreateLevelConnections) and generates the town.
return DRLG_AllocDrlg(pAct, ACT_I, nullptr, nSeed, LEVEL_ROGUEENCAMPMENT, 0, nullptr, nDifficulty, nullptr, nullptr);
}
std::vector<int> ParseLevels(const char* sz)
{
std::vector<int> levels;
std::stringstream ss(sz);
std::string item;
while (std::getline(ss, item, ','))
{
if (item.empty()) continue;
levels.push_back(atoi(item.c_str()));
}
return levels;
}
} // namespace
int main(int argc, char** argv)
{
const char* szOut = nullptr;
const char* szTrace = nullptr;
bool bHaveSeed = false;
uint32_t nSeed = 0;
int nDifficulty = 0;
bool bIsolated = false;
std::vector<int> levels;
for (int i = 1; i < argc; ++i)
{
const std::string arg = argv[i];
auto next = [&]() -> const char* {
if (i + 1 >= argc) D2ORACLE_Fail("missing value for %s", arg.c_str());
return argv[++i];
};
if (arg == "--data") gsD2OracleDataDir = next();
else if (arg == "--seed") { nSeed = (uint32_t)strtoul(next(), nullptr, 0); bHaveSeed = true; }
else if (arg == "--levels") levels = ParseLevels(next());
else if (arg == "--difficulty") nDifficulty = atoi(next());
else if (arg == "--isolated") bIsolated = true;
else if (arg == "--trace") szTrace = next();
else if (arg == "--out") szOut = next();
else D2ORACLE_Fail("unknown argument %s", arg.c_str());
}
// Without --levels only the act stage (layout, links, town) is generated and dumped.
if (gsD2OracleDataDir.empty() || !bHaveSeed || !szOut)
{
D2ORACLE_Fail("usage: d2moo-oracle --data <dir> --seed <n> [--levels 2,3] --out <file> [--difficulty n] [--isolated] [--trace file]");
}
if (nDifficulty < 0 || nDifficulty > 2)
{
D2ORACLE_Fail("difficulty must be 0..2");
}
D2ORACLE_LoadDataTables();
if (szTrace)
{
D2ORACLE_OpenTrace(szTrace);
}
D2DrlgActStrc act = {};
std::ostringstream o;
o << "{\"schema\":3,\"oracle\":{\"d2moo\":\"" << D2ORACLE_D2MOO_COMMIT << "\",\"arch\":\"i386\"},\"gameSeed\":" << nSeed
<< ",\"difficulty\":" << nDifficulty << ",\"isolated\":" << (bIsolated ? "true" : "false");
D2DrlgStrc* pDrlg = AllocAct1(&act, nSeed, (uint8_t)nDifficulty);
o << ",\"drlg\":{\"startSeed\":" << pDrlg->dwStartSeed << ",\"seed\":" << Seed(pDrlg->pSeed) << "},\"act\":" << Act(pDrlg) << ",\"levels\":[";
std::vector<std::string> heads(levels.size());
std::vector<D2DrlgLevelStrc*> generated(levels.size(), nullptr);
std::vector<std::vector<RoomActivation>> activations(levels.size());
if (bIsolated)
{
// A fresh act per level proves level generation does not depend on other levels.
for (size_t i = 0; i < levels.size(); ++i)
{
if (i > 0)
{
pDrlg = AllocAct1(&act, nSeed, (uint8_t)nDifficulty);
}
heads[i] = InitLevelStage(pDrlg, levels[i], &generated[i]);
activations[i] = Activate(generated[i]);
}
}
else
{
// Activating a room initialises the levels behind its warps/vis links (sub_6FD77BB0 ->
// DRLG_InitLevel), so every requested level is generated before any room is activated.
for (size_t i = 0; i < levels.size(); ++i)
{
heads[i] = InitLevelStage(pDrlg, levels[i], &generated[i]);
}
for (size_t i = 0; i < levels.size(); ++i)
{
activations[i] = Activate(generated[i]);
}
}
// "mapTiles" is serialised only now: a room activated later rewrites tiles of the near rooms
// activated before it and rolls their seeds (DRLGROOMTILE_GetCreateLinkedTileData ->
// DRLGROOMTILE_LinkedTileDataManager), also across level seams, so the tile data are the final state.
std::map<D2DrlgStrc*, RefIndex> refIndices;
for (size_t i = 0; i < levels.size(); ++i)
{
D2DrlgStrc* pLevelDrlg = generated[i]->pDrlg;
auto it = refIndices.find(pLevelDrlg);
if (it == refIndices.end())
{
it = refIndices.emplace(pLevelDrlg, BuildRefIndex(pLevelDrlg)).first;
}
o << (i ? "," : "") << heads[i] << ",\"activation\":[";
for (size_t k = 0; k < activations[i].size(); ++k)
{
const RoomActivation& a = activations[i][k];
o << (k ? "," : "") << a.szHead << ",\"seedAfterTiles\":" << a.szSeedAfterTiles << ",\"mapTiles\":"
<< MapTiles(it->second, a.pRoom, generated[i]->nLevelId, (int)k) << "}";
}
o << "]}";
}
o << "]}\n";
D2ORACLE_CloseTrace();
FILE* f = fopen(szOut, "wb");
if (!f)
{
D2ORACLE_Fail("cannot write %s", szOut);
}
const std::string json = o.str();
fwrite(json.data(), 1, json.size(), f);
fclose(f);
return 0;
}