Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,7 @@ set(SOURCE_FILES
client/uiminimap.cpp
client/uiprogressrect.cpp
client/uisprite.cpp
tools/datdump.cpp
)

if (TOGGLE_FRAMEWORK_GRAPHICS)
Expand Down
42 changes: 42 additions & 0 deletions src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@
#include "framework/core/resourcemanager.h"
#include "framework/luaengine/luainterface.h"
#include "framework/platform/platform.h"
#include "tools/datdump.h"

#include <algorithm>
#include <array>
#include <iostream>
#include <optional>

#ifndef ANDROID
#if ENABLE_DISCORD_RPC == 1
Expand All @@ -43,13 +49,40 @@
extern "C" {
#endif

namespace {

bool shouldShowHelp(const std::vector<std::string>& args)
{
for (const auto& arg : args) {
if (arg == "--help" || arg == "-h" || arg == "/?")
return true;
}
return false;
}

void printHelp(const std::string& executableName)
{
std::cout << "Usage: " << executableName << " [options]\n\n"
"General options:\n"
" --help, -h, /? Show this help message and exit\n"
" --encrypt <password> Encrypt assets (requires builder build)\n\n"
"DAT debugging:\n"
" --dump-dat-to-json=<path> Dump the specified Tibia DAT file as JSON\n"
" --dump-dat-output=<path> Write JSON to file instead of stdout\n"
" --dump-dat-client-version=<ver> Decode flags using that client version\n"
" --dump-dat-compact Emit compact (single-line) JSON\n";
}

} // namespace

int main(const int argc, const char* argv[])
{
std::vector<std::string> args(argv, argv + argc);

// process args encoding
g_platform.init(args);


// initialize resources
#ifdef ANDROID
// Unzip Android assets/data.zip
Expand Down Expand Up @@ -79,6 +112,15 @@ extern "C" {
if (!g_resources.discoverWorkDir("init.lua"))
g_logger.fatal("Unable to find work directory, the application cannot be initialized.");

if (shouldShowHelp(args)) {
printHelp(args[0]);
return 0;
}

if (const auto dumpRequest = datdump::parseRequest(args); dumpRequest) {
return datdump::run(*dumpRequest) ? 0 : 1;
}

// initialize application framework and otclient
const auto drawEvents = ApplicationDrawEventsPtr(&g_client, [](ApplicationDrawEvents*) {});
g_app.init(args, new GraphicalApplicationContext(g_gameConfig.getSpriteSize(), drawEvents));
Expand Down
225 changes: 225 additions & 0 deletions src/tools/datdump.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
/*
* Copyright (c) 2010-2025 OTClient <https://github.com/edubart/otclient>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

#include "tools/datdump.h"

#include "client/game.h"
#include "client/thingtype.h"
#include "client/thingtypemanager.h"
#include "framework/luaengine/luainterface.h"

#include <nlohmann/json.hpp>

#include <array>
#include <fstream>
#include <iostream>
#include <optional>
#include <string>
#include <string_view>
#include <stdexcept>
#include <limits>
namespace datdump {

using json = nlohmann::ordered_json;

namespace {

std::optional<std::string> readFlagValue(std::vector<std::string>& args, const size_t index, const std::string_view flag)
{
const bool hasInlineValue = args[index].starts_with(flag) && args[index].size() > flag.size() && args[index][flag.size()] == '=';
if (hasInlineValue)
return args[index].substr(flag.size() + 1);

if (args[index] == flag) {
if (index + 1 >= args.size())
return std::nullopt;
std::string value = args[index + 1];
args.erase(args.begin() + static_cast<long>(index + 1));
return value;
}

return std::nullopt;
}

std::string categoryToString(const ThingCategory category)
{
switch (category) {
case ThingCategoryItem: return "items";
case ThingCategoryCreature: return "creatures";
case ThingCategoryEffect: return "effects";
case ThingCategoryMissile: return "missiles";
default: return "unknown (fixme bug in the source code!)";
}
}

json thingTypeToJson(const ThingTypePtr& type, const ThingCategory category, const uint16_t id)
{
json entry;
entry["id"] = id;
entry["category"] = categoryToString(category);
entry["name"] = type->getName();
if (const auto& description = type->getDescription(); !description.empty())
entry["description"] = description;

entry["size"] = {
{ "width", type->getWidth() },
{ "height", type->getHeight() },
{ "layers", type->getLayers() },
};
entry["patterns"] = {
{ "x", type->getNumPatternX() },
{ "y", type->getNumPatternY() },
{ "z", type->getNumPatternZ() },
};
entry["animationPhases"] = type->getAnimationPhases();
entry["groundSpeed"] = type->getGroundSpeed();
entry["elevation"] = type->getElevation();

auto& flags = entry["flags"];
flags["floorChange"] = type->hasAttr(ThingAttrFloorChange);
flags["ground"] = type->isGround();
flags["groundBorder"] = type->isGroundBorder();
flags["onTop"] = type->isOnTop();
flags["onBottom"] = type->isOnBottom();
flags["fullGround"] = type->isFullGround();
flags["notWalkable"] = type->isNotWalkable();
flags["notPathable"] = type->isNotPathable();
flags["blockProjectile"] = type->blockProjectile();
flags["hasElevation"] = type->hasAttr(ThingAttrElevation);
flags["hasDisplacement"] = type->hasAttr(ThingAttrDisplacement);
flags["hasLight"] = type->hasAttr(ThingAttrLight);

return entry;
}

} // namespace

std::optional<Request> parseRequest(std::vector<std::string>& args)
{
for (size_t i = 1; i < args.size(); ++i) {
if (!args[i].starts_with("--dump-dat-to-json"))
continue;

Request request;
if (auto datValue = readFlagValue(args, i, "--dump-dat-to-json"); datValue) {
request.datPath = *datValue;
} else {
throw std::runtime_error("--dump-dat-to-json requires an argument (e.g. --dump-dat-to-json=data/Tibia.dat)");
}

args.erase(args.begin() + static_cast<long>(i));

for (size_t j = 1; j < args.size();) {
if (auto value = readFlagValue(args, j, "--dump-dat-output"); value) {
request.outputPath = *value;
args.erase(args.begin() + static_cast<long>(j));
continue;
}

if (auto value = readFlagValue(args, j, "--dump-dat-client-version"); value) {
try {
request.clientVersion = std::stoi(*value);
} catch (const std::exception&) {
std::throw_with_nested(
std::invalid_argument("invalid --dump-dat-client-version value")
);
}
args.erase(args.begin() + static_cast<long>(j));
continue;
}

if (args[j] == "--dump-dat-compact") {
request.compactOutput = true;
args.erase(args.begin() + static_cast<long>(j));
continue;
}
throw std::runtime_error("unknown datdump argument: " + args[j]);
//++j;
}

return request;
}

return std::nullopt;
}

bool run(const Request& request)
{
if (request.datPath.empty()) {
throw std::invalid_argument("--dump-dat-to-json requires a DAT file path (e.g. --dump-dat-to-json=data/Tibia.dat)");
}

g_lua.init();
g_things.init();

if (request.clientVersion > 0)
g_game.setClientVersion(static_cast<uint16_t>(request.clientVersion));

if (!g_things.loadDat(request.datPath)) {
throw std::runtime_error("unable to load DAT file: " + request.datPath);
}

json root;
root["datSignature"] = g_things.getDatSignature();
root["contentRevision"] = g_things.getContentRevision();
root["clientVersion"] = g_game.getClientVersion();

const std::array<ThingCategory, 4> categories{ ThingCategoryItem, ThingCategoryCreature, ThingCategoryEffect, ThingCategoryMissile };
json categoriesJson;
for (const auto category : categories) {
const auto& list = g_things.getThingTypes(category);
if (list.size() > std::numeric_limits<uint16_t>::max() + 1ULL) {
throw std::runtime_error("thing list for category " + categoryToString(category) + " exceeds uint16_t range (" + std::to_string(list.size() - 1) + " entries)");
}

json entries = json::array();
for (size_t idx = 1; idx < list.size(); ++idx) {
const auto& type = list[idx];
if (!type || type->isNull())
continue;
entries.emplace_back(thingTypeToJson(type, category, static_cast<uint16_t>(idx)));
}
categoriesJson[categoryToString(category)] = std::move(entries);
}
root["categories"] = std::move(categoriesJson);

const int indent = request.compactOutput ? -1 : 2;
const auto payload = indent > 0 ? root.dump(indent) : root.dump();

bool success = true;
if (request.outputPath.empty()) {
std::cout << payload << std::endl;
} else {
std::ofstream out(request.outputPath, std::ios::out | std::ios::trunc);
if (!out) {
throw std::runtime_error("unable to open output file: " + request.outputPath);
} else {
out << payload << '\n';
}
}

g_things.terminate();
g_lua.terminate();
return success;
}

} // namespace datdump
42 changes: 42 additions & 0 deletions src/tools/datdump.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/*
* Copyright (c) 2010-2025 OTClient <https://github.com/edubart/otclient>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
* THE SOFTWARE.
*/

#pragma once

#include <optional>
#include <string>
#include <vector>

namespace datdump {

struct Request
{
std::string datPath;
std::string outputPath;
int clientVersion{ 0 };
bool compactOutput{ false };
};

std::optional<Request> parseRequest(std::vector<std::string>& args);
bool run(const Request& request);

} // namespace datdump
Loading