24 namespace fs = std::filesystem;
25 constexpr std::uintmax_t
PayloadLimit = 512ULL * 1024ULL * 1024ULL;
27 constexpr std::array
Banks{
28 Bank{
"dat_main",
"dat_main.h", 2},
Bank{
"dat_pat",
"dat_pat.h", 3},
29 Bank{
"dat_bord",
"dat_bord.h", 6},
Bank{
"dat_brd2",
"dat_brd2.h", 7},
30 Bank{
"dat_3d",
"dat_3d.h", 8},
Bank{
"dat_lm01",
"dat_lm01.h", 5},
31 Bank{
"dat_lk01",
"dat_lk01.h", 10}};
40 const auto value =
path.generic_u8string();
41 return {
reinterpret_cast<const char*
>(
value.data()),
value.size()};
44 {
return fs::path(std::u8string(
value.begin(),
value.end())); }
45 bool inside(
const fs::path& parent,
const fs::path&
child)
47 auto p = parent.begin(),
c =
child.begin();
48 for (;
p != parent.end(); ++
p, ++
c)
50 if (
c ==
child.end())
return false;
54 if (*
p != *
c)
return false;
60 {
return fs::weakly_canonical(fs::absolute(
path)); }
64 throw std::runtime_error(
"output and input trees must not overlap: " +
pathText(
input));
71 if (
c ==
'\t')
out +=
"\\t";
72 else if (
c ==
'\r')
out +=
"\\r";
73 else if (
c ==
'\n')
out +=
"\\n";
74 else if (
c ==
'\\')
out +=
"\\\\";
79 std::ofstream report(
const fs::path&
path)
81 std::ofstream
out(
path, std::ios::binary);
83 out.exceptions(std::ios::badbit | std::ios::failbit);
88 const auto size = fs::file_size(
path);
90 throw std::runtime_error(
"payload must be nonempty and at most 512 MiB");
92 std::ifstream in(
path, std::ios::binary);
93 if (!in.read(
reinterpret_cast<char*
>(
bytes.data()),
static_cast<std::streamsize
>(
bytes.size())) ||
94 in.peek() != std::char_traits<char>::eof())
95 throw std::runtime_error(
"payload read failed or size changed: " +
pathText(
path));
98 std::uint32_t
u32(std::span<const std::byte>
bytes, std::size_t
n)
100 return std::to_integer<std::uint32_t>(
bytes[
n]) |
101 (std::to_integer<std::uint32_t>(
bytes[
n+1]) << 8U) |
102 (std::to_integer<std::uint32_t>(
bytes[
n+2]) << 16U) |
103 (std::to_integer<std::uint32_t>(
bytes[
n+3]) << 24U);
105 bool magic(std::span<const std::byte>
bytes, std::size_t
n, std::string_view
wanted)
108 for (std::size_t
i = 0;
i <
wanted.size(); ++
i)
109 if (std::to_integer<unsigned char>(
bytes[
n+
i]) !=
static_cast<unsigned char>(
wanted[
i]))
return false;
119 if (!result)
throw std::runtime_error(result.error().detail);
124 if (!result)
throw std::runtime_error(result.error().detail);
128 LegacyChunkReader reader{std::span<const std::byte>(
bytes)};
129 const auto first = reader.descend();
130 if (!
first)
throw std::runtime_error(
first.error().detail);
132 throw std::runtime_error(
"CNK must contain one complete root sequence");
137 if (!result)
throw std::runtime_error(result.error().detail);
142 throw std::runtime_error(
"WAV payload has no RIFF/WAVE header");
143 const std::uint64_t end = 8ULL +
u32(
bytes, 4);
144 if (end !=
bytes.size())
throw std::runtime_error(
"RIFF size does not match payload");
146 for (std::uint64_t
p = 12;
p < end;)
148 if (end -
p < 8)
throw std::runtime_error(
"truncated RIFF chunk header");
149 const auto at =
static_cast<std::size_t
>(
p);
152 if (
next > end)
throw std::runtime_error(
"RIFF chunk exceeds payload");
157 if (!
format || !data)
throw std::runtime_error(
"WAV is missing format or audio data");
159 else throw std::runtime_error(
"unsupported manifest payload type");
163 using FileIndex = std::map<std::string, std::vector<File>>;
167 for (std::size_t
i = 0;
i <
roots.size(); ++
i)
169 if (!fs::is_directory(
roots[
i]))
170 throw std::runtime_error(
"resource root is not a directory: " +
pathText(
roots[
i]));
173 if (
entry.is_symlink() || !
entry.is_regular_file())
continue;
183 std::ranges::sort(
files, [](
const File&
a,
const File&
b) {
184 if (
a.root !=
b.root)
return a.root <
b.root;
185 return a.relative <
b.relative;
190 using MappingKey = std::pair<std::string, std::string>;
191 using Mapping = std::map<MappingKey, File>;
193 const std::set<MappingKey>&
allowed)
196 if (!
path)
return result;
197 if (fs::file_size(*
path) > 16U * 1024U * 1024U)
198 throw std::runtime_error(
"resource mapping exceeds 16 MiB");
199 std::ifstream in(*
path);
200 if (!in)
throw std::runtime_error(
"cannot open resource mapping");
203 while (std::getline(in, line))
206 if (!line.empty() && line.back() ==
'\r') line.pop_back();
207 if (line.empty() || line[0] ==
'#')
continue;
208 if (
lineNumber == 1 && line ==
"bank\tsymbol\troot\tpath")
continue;
209 const auto a = line.find(
'\t'),
b = line.find(
'\t',
a == line.npos ?
a :
a+1);
210 const auto c = line.find(
'\t',
b == line.npos ?
b :
b+1);
211 if (
a == line.npos ||
b == line.npos ||
c == line.npos || line.find(
'\t',
c+1) != line.npos)
212 throw std::runtime_error(
"mapping must have four TSV columns at line " + std::to_string(
lineNumber));
214 if (!
allowed.contains(
key))
throw std::runtime_error(
"unknown bank/symbol in mapping: " + line.substr(0,
b));
215 const auto rootNumber = std::string_view(line).substr(
b+1,
c-
b-1);
219 throw std::runtime_error(
"invalid root index in mapping line " + std::to_string(
lineNumber));
222 throw std::runtime_error(
"mapping path must be relative to its resource root");
225 throw std::runtime_error(
"mapped file missing or outside resource root: " +
pathText(
relative));
226 if (!result.emplace(
key, File{rootIndex, relative, absolute}).second)
227 throw std::runtime_error(
"duplicate bank/symbol mapping");
229 if (in.bad())
throw std::runtime_error(
"mapping read failed");
232 std::vector<std::string>
namesFor(
const LegacyManifestEntry&
entry)
244 default:
throw std::runtime_error(
"unsupported manifest type");
246 std::vector<std::string> names;
255 std::uint16_t
group,
const std::vector<ArchiveBuildItem>& items)
257 if (
bank.available == 0)
return {};
258 const auto folder =
run/(
bank.complete() ?
"Dat_Mon" :
"partial");
259 fs::create_directories(
folder);
262 pending +=
".writing";
265 ArchiveOpenOptions options;
268 if (!
opened)
throw std::runtime_error(
opened.error().detail);
269 if ((*opened)->itemCount() != items.size())
270 throw std::runtime_error(
"archive read-back changed the number of tags");
271 for (std::size_t tag = 0; tag < items.size(); ++tag)
273 const auto meta = (*opened)->metadata(
static_cast<DataTag>(tag));
274 if (!
meta ||
meta->type != items[tag].type ||
meta->present() == items[tag].emptySlot())
275 throw std::runtime_error(
"archive read-back changed a tag or type");
276 if (items[tag].emptySlot())
continue;
277 const auto payload = (*opened)->load(
static_cast<DataTag>(tag));
278 if (!payload || **payload != items[tag].payload)
279 throw std::runtime_error(
"archive read-back changed resource bytes");
284 if (!language)
throw std::runtime_error(language.error().detail);
287 fs::rename(pending,
target);
290 std::string
joined(
const std::vector<std::string>& values)
293 for (
const auto&
value : values) {
if (!result.empty()) result +=
" | "; result +=
value; }
308#ifdef MONOPOLY_PROTECTED_SOURCE_ROOT
312 std::map<std::string, LegacyBankManifest>
manifests;
323 fs::create_directories(
output);
327 const auto stamp = std::chrono::system_clock::now().time_since_epoch().count();
331 if (result.
directory.empty())
throw std::runtime_error(
"cannot create an exclusive assembly directory");
333 details <<
"bank\ttag_or_message_id\ttype\tsymbol\tstatus\troot\tpath\tdetail\n";
335 bankReport <<
"bank\texpected\tavailable\tinvalid\tstatus\tarchive\n";
336 std::set<fs::path> consumed;
338 const auto status =
bank.complete() ?
"MANIFEST_COMPLETE" :
"INCOMPLETE";
341 std::cout <<
bank.name <<
": " <<
bank.available <<
'/' <<
bank.expected <<
" " << status
342 <<
"; invalid=" <<
bank.invalid <<
'\n';
344 for (
const auto& definition :
Banks)
348 std::vector<ArchiveBuildItem> items(
manifest.declaredItemCount);
358 std::set<fs::path>
seen;
361 for (
const auto& file :
found->second)
362 if (
seen.insert(file.absolute).second)
candidates.push_back(file);
364 std::string status,
why;
370 for (
const auto& file :
candidates)
why += std::to_string(file.root)+
":"+
pathText(file.relative)+
" | ";
378 const auto size = fs::file_size(file.absolute);
380 throw std::runtime_error(
"bank payload budget exceeds 512 MiB");
386 consumed.insert(file.absolute);
388 why=
explicitMapping ?
"explicit mapping; format inspected" :
"unique exact filename; format inspected";
390 catch (
const std::exception&
error) { status=
"invalid";
why=
error.what(); ++
bank.invalid; }
400 for (
const auto& [
id,
name] :
english.requiredNames)
402 const bool present=
english.text.contains(
id);
403 details << language.name <<
'\t' <<
id <<
"\tString\t" <<
name <<
'\t'
404 << (present ?
"packed" :
"missing") <<
"\t0\tEnglish.atr\t"
405 << (present ?
"exact symbolic ID; ASCII to UTF-16LE" :
"no exact symbolic text in supplied ATR") <<
'\n';
409 <<
"\tunmapped-symbol\t0\tEnglish.atr\tnot defined in active dat_lang.h; no numeric ID guessed\n";
412 result.
banks.push_back(std::move(language));
414 unused <<
"root\tpath\treason\n";
415 const std::set<std::string>
extensions{
".bmp",
".tab",
".uap",
".wav",
".cnk",
".hmd"};
419 for (
const auto& file :
matches)
422 <<
"\tno unique manifest assignment; may already be used as an external texture\n";
425 summary <<
"Monopoly archive assembly\n"
426 << (result.
complete() ?
"All manifest entries have payloads.\n" :
"INCOMPLETE - not a playable game data set.\n")
427 <<
"No files were modified in Source/ or installed in the game's Dat_Mon.\n"
428 <<
"Partial archives contain ONLY recovered payloads. Original numeric tags are preserved.\n"
429 <<
"A missing slot is not a substitute for an animation, sound, model or image.\n"
430 <<
"Dat_Mon/ contains only banks complete against their manifests; partial/ is never installed.\n"
431 <<
"Language conversion only maps exact symbols shared by English.atr and dat_lang.h.\n"
432 <<
"No numbers from English.A or unrelated legacy headers were assumed equivalent.\n"
433 <<
"resources.tsv lists every resource decision. banks.tsv summarizes banks.\n"
434 <<
"unassigned-files.tsv also lists external texture files; unassigned does NOT mean missing.\n"
435 <<
"Container CRC and byte-for-byte read-back are checked during publication.\n"
436 <<
"Payload structural inspection does not establish cross-bank references or gameplay fidelity.\n";
440 << (result.
complete() ?
"ASSEMBLED; gameplay not qualified\n" :
"INCOMPLETE; runtime installation left untouched\n");
std::vector< std::byte > bytes