2
0
mirror of https://github.com/Laupetin/OpenAssetTools.git synced 2026-01-24 08:53:04 +00:00

chore: restructure Linker and Unlinker for system testing

This commit is contained in:
Jan Laupetin
2025-12-23 12:49:22 +01:00
parent fd9c57e15a
commit a1693b2eb8
32 changed files with 1089 additions and 982 deletions

View File

@@ -0,0 +1,24 @@
#include "ContentPrinter.h"
#include "Utils/Logging/Log.h"
#include <format>
#include <iostream>
ContentPrinter::ContentPrinter(const Zone& zone)
: m_zone(zone)
{
}
void ContentPrinter::PrintContent() const
{
const auto* pools = m_zone.m_pools.get();
const auto* game = IGame::GetGameById(m_zone.m_game_id);
con::info("Zone '{}' ({})", m_zone.m_name, game->GetShortName());
con::info("Content:");
for (const auto& asset : *pools)
con::info("{}, {}", *pools->GetAssetTypeName(asset->m_type), asset->m_name);
con::info("");
}

View File

@@ -0,0 +1,14 @@
#pragma once
#include "Zone/Zone.h"
class ContentPrinter
{
public:
explicit ContentPrinter(const Zone& zone);
void PrintContent() const;
private:
const Zone& m_zone;
};

327
src/Unlinking/Unlinker.cpp Normal file
View File

@@ -0,0 +1,327 @@
#include "Unlinker.h"
#include "ContentLister/ContentPrinter.h"
#include "IObjLoader.h"
#include "IObjWriter.h"
#include "ObjWriting.h"
#include "SearchPath/IWD.h"
#include "SearchPath/OutputPathFilesystem.h"
#include "UnlinkerArgs.h"
#include "UnlinkerPaths.h"
#include "Utils/ClassUtils.h"
#include "Utils/Logging/Log.h"
#include "Utils/ObjFileStream.h"
#include "Zone/Definition/ZoneDefWriter.h"
#include "ZoneLoading.h"
#include <cassert>
#include <filesystem>
#include <format>
#include <fstream>
#include <regex>
namespace fs = std::filesystem;
namespace
{
class UnlinkerImpl : public Unlinker
{
public:
UnlinkerImpl(UnlinkerArgs args)
: m_args(std::move(args))
{
}
bool Start() override
{
UnlinkerPaths paths;
if (!paths.LoadUserPaths(m_args))
return false;
if (!LoadZones(paths))
return false;
const auto result = UnlinkZones(paths);
UnloadZones();
return result;
}
private:
[[nodiscard]] bool ShouldLoadObj() const
{
return m_args.m_task != UnlinkerArgs::ProcessingTask::LIST && !m_args.m_skip_obj;
}
[[nodiscard]] bool WriteZoneDefinitionFile(const Zone& zone, const fs::path& zoneDefinitionFileFolder) const
{
auto zoneDefinitionFilePath(zoneDefinitionFileFolder);
zoneDefinitionFilePath.append(zone.m_name);
zoneDefinitionFilePath.replace_extension(".zone");
std::ofstream zoneDefinitionFile(zoneDefinitionFilePath, std::fstream::out | std::fstream::binary);
if (!zoneDefinitionFile.is_open())
{
con::error("Failed to open file for zone definition file of zone \"{}\".", zone.m_name);
return false;
}
const auto* zoneDefWriter = IZoneDefWriter::GetZoneDefWriterForGame(zone.m_game_id);
zoneDefWriter->WriteZoneDef(zoneDefinitionFile, zone, m_args.m_use_gdt);
zoneDefinitionFile.close();
return true;
}
static bool OpenGdtFile(const Zone& zone, const fs::path& outputFolder, std::ofstream& stream)
{
auto gdtFilePath(outputFolder);
gdtFilePath.append("source_data");
fs::create_directories(gdtFilePath);
gdtFilePath.append(zone.m_name);
gdtFilePath.replace_extension(".gdt");
stream = std::ofstream(gdtFilePath, std::fstream::out | std::fstream::binary);
if (!stream.is_open())
{
con::error("Failed to open file for zone definition file of zone \"{}\".", zone.m_name);
return false;
}
return true;
}
void UpdateAssetIncludesAndExcludes(const AssetDumpingContext& context) const
{
const auto assetTypeCount = context.m_zone.m_pools->GetAssetTypeCount();
ObjWriting::Configuration.AssetTypesToHandleBitfield = std::vector<bool>(assetTypeCount);
std::vector<bool> handledSpecifiedAssets(m_args.m_specified_asset_types.size());
for (auto i = 0u; i < assetTypeCount; i++)
{
const auto assetTypeName = std::string(*context.m_zone.m_pools->GetAssetTypeName(i));
const auto foundSpecifiedEntry = m_args.m_specified_asset_type_map.find(assetTypeName);
if (foundSpecifiedEntry != m_args.m_specified_asset_type_map.end())
{
ObjWriting::Configuration.AssetTypesToHandleBitfield[i] = m_args.m_asset_type_handling == UnlinkerArgs::AssetTypeHandling::INCLUDE;
assert(foundSpecifiedEntry->second < handledSpecifiedAssets.size());
handledSpecifiedAssets[foundSpecifiedEntry->second] = true;
}
else
ObjWriting::Configuration.AssetTypesToHandleBitfield[i] = m_args.m_asset_type_handling == UnlinkerArgs::AssetTypeHandling::EXCLUDE;
}
auto anySpecifiedValueInvalid = false;
for (auto i = 0u; i < handledSpecifiedAssets.size(); i++)
{
if (!handledSpecifiedAssets[i])
{
con::error("Unknown asset type \"{}\"", m_args.m_specified_asset_types[i]);
anySpecifiedValueInvalid = true;
}
}
if (anySpecifiedValueInvalid)
{
con::error("Valid asset types are:");
auto first = true;
std::ostringstream ss;
for (auto i = 0u; i < assetTypeCount; i++)
{
const auto assetTypeName = std::string(*context.m_zone.m_pools->GetAssetTypeName(i));
if (first)
first = false;
else
ss << ", ";
ss << assetTypeName;
}
con::error(ss.str());
}
}
/**
* \brief Performs the tasks specified by the command line arguments on the specified zone.
* \param searchPath The search path for obj data.
* \param zone The zone to handle.
* \return \c true if handling the zone was successful, otherwise \c false
*/
bool HandleZone(ISearchPath& searchPath, Zone& zone) const
{
if (m_args.m_task == UnlinkerArgs::ProcessingTask::LIST)
{
const ContentPrinter printer(zone);
printer.PrintContent();
}
else if (m_args.m_task == UnlinkerArgs::ProcessingTask::DUMP)
{
const auto outputFolderPathStr = m_args.GetOutputFolderPathForZone(zone);
const fs::path outputFolderPath(outputFolderPathStr);
fs::create_directories(outputFolderPath);
fs::path zoneDefinitionFileFolder(outputFolderPath);
zoneDefinitionFileFolder.append("zone_source");
fs::create_directories(zoneDefinitionFileFolder);
if (!WriteZoneDefinitionFile(zone, zoneDefinitionFileFolder))
return false;
OutputPathFilesystem outputFolderOutputPath(outputFolderPath);
AssetDumpingContext context(zone, outputFolderPathStr, outputFolderOutputPath, searchPath, std::nullopt);
std::ofstream gdtStream;
if (m_args.m_use_gdt)
{
if (!OpenGdtFile(zone, outputFolderPath, gdtStream))
return false;
auto gdt = std::make_unique<GdtOutputStream>(gdtStream);
gdt->BeginStream();
const auto* game = IGame::GetGameById(zone.m_game_id);
gdt->WriteVersion(GdtVersion(game->GetShortName(), 1));
context.m_gdt = std::move(gdt);
}
UpdateAssetIncludesAndExcludes(context);
const auto* objWriter = IObjWriter::GetObjWriterForGame(zone.m_game_id);
auto result = objWriter->DumpZone(context);
if (m_args.m_use_gdt)
{
context.m_gdt->EndStream();
gdtStream.close();
}
if (!result)
{
con::error("Dumping zone failed!");
return false;
}
}
return true;
}
bool LoadZones(UnlinkerPaths& paths)
{
for (const auto& zonePath : m_args.m_zones_to_load)
{
if (!fs::is_regular_file(zonePath))
{
con::error("Could not find file \"{}\".", zonePath);
continue;
}
auto absoluteZoneDirectory = absolute(std::filesystem::path(zonePath).remove_filename()).string();
auto searchPathsForZone = paths.GetSearchPathsForZone(absoluteZoneDirectory);
auto maybeZone = ZoneLoading::LoadZone(zonePath, std::nullopt);
if (!maybeZone)
{
con::error("Failed to load zone \"{}\": {}", zonePath, maybeZone.error());
return false;
}
auto zone = std::move(*maybeZone);
con::debug("Loaded zone \"{}\"", zone->m_name);
if (ShouldLoadObj())
{
const auto* objLoader = IObjLoader::GetObjLoaderForGame(zone->m_game_id);
objLoader->LoadReferencedContainersForZone(*searchPathsForZone, *zone);
}
m_loaded_zones.emplace_back(std::move(zone));
}
return true;
}
void UnloadZones()
{
for (auto i = m_loaded_zones.rbegin(); i != m_loaded_zones.rend(); ++i)
{
auto& loadedZone = *i;
// Copy zone name since we deallocate before logging
const auto zoneName = loadedZone->m_name;
if (ShouldLoadObj())
{
const auto* objLoader = IObjLoader::GetObjLoaderForGame(loadedZone->m_game_id);
objLoader->UnloadContainersOfZone(*loadedZone);
}
loadedZone.reset();
con::debug("Unloaded zone \"{}\"", zoneName);
}
m_loaded_zones.clear();
}
bool UnlinkZones(UnlinkerPaths& paths) const
{
for (const auto& zonePath : m_args.m_zones_to_unlink)
{
if (!fs::is_regular_file(zonePath))
{
con::error("Could not find file \"{}\".", zonePath);
continue;
}
auto zoneDirectory = fs::path(zonePath).remove_filename();
if (zoneDirectory.empty())
zoneDirectory = fs::current_path();
auto absoluteZoneDirectory = absolute(zoneDirectory).string();
auto searchPathsForZone = paths.GetSearchPathsForZone(absoluteZoneDirectory);
auto maybeZone = ZoneLoading::LoadZone(zonePath, std::nullopt);
if (!maybeZone)
{
con::error("Failed to load zone \"{}\": {}", zonePath, maybeZone.error());
return false;
}
auto zone = std::move(*maybeZone);
con::debug("Loaded zone \"{}\"", zone->m_name);
const auto* objLoader = IObjLoader::GetObjLoaderForGame(zone->m_game_id);
if (ShouldLoadObj())
objLoader->LoadReferencedContainersForZone(*searchPathsForZone, *zone);
if (!HandleZone(*searchPathsForZone, *zone))
return false;
if (ShouldLoadObj())
objLoader->UnloadContainersOfZone(*zone);
// Copy zone name for using it after freeing the zone
std::string zoneName = zone->m_name;
zone.reset();
con::debug("Unloaded zone \"{}\"", zoneName);
}
return true;
}
UnlinkerArgs m_args;
std::vector<std::unique_ptr<Zone>> m_loaded_zones;
};
} // namespace
std::unique_ptr<Unlinker> Unlinker::Create(UnlinkerArgs args)
{
return std::make_unique<UnlinkerImpl>(std::move(args));
}

19
src/Unlinking/Unlinker.h Normal file
View File

@@ -0,0 +1,19 @@
#pragma once
#include "UnlinkerArgs.h"
#include <memory>
class Unlinker
{
public:
virtual ~Unlinker() = default;
static std::unique_ptr<Unlinker> Create(UnlinkerArgs args);
/**
* \brief Starts the Unlinker application logic.
* \return \c true if the application was successful or \c false if an error occurred.
*/
virtual bool Start() = 0;
};

View File

@@ -0,0 +1,394 @@
#include "UnlinkerArgs.h"
#include "GitVersion.h"
#include "ObjLoading.h"
#include "ObjWriting.h"
#include "Utils/Arguments/UsageInformation.h"
#include "Utils/FileUtils.h"
#include "Utils/Logging/Log.h"
#include "Utils/StringUtils.h"
#include <format>
#include <iostream>
#include <regex>
#include <type_traits>
// clang-format off
const CommandLineOption* const OPTION_HELP =
CommandLineOption::Builder::Create()
.WithShortName("?")
.WithLongName("help")
.WithDescription("Displays usage information.")
.Build();
const CommandLineOption* const OPTION_VERSION =
CommandLineOption::Builder::Create()
.WithLongName("version")
.WithDescription("Prints the application version.")
.Build();
const CommandLineOption* const OPTION_VERBOSE =
CommandLineOption::Builder::Create()
.WithShortName("v")
.WithLongName("verbose")
.WithDescription("Outputs a lot more and more detailed messages.")
.Build();
const CommandLineOption* const OPTION_NO_COLOR =
CommandLineOption::Builder::Create()
.WithLongName("no-color")
.WithDescription("Disables colored terminal output.")
.Build();
const CommandLineOption* const OPTION_MINIMAL_ZONE_FILE =
CommandLineOption::Builder::Create()
.WithShortName("min")
.WithLongName("minimal-zone")
.WithDescription("Minimizes the size of the zone file output by only including assets that are not a dependency of another asset.")
.Build();
const CommandLineOption* const OPTION_LOAD =
CommandLineOption::Builder::Create()
.WithShortName("l")
.WithLongName("load")
.WithDescription("Loads an existing zone before trying to unlink any zone.")
.WithParameter("zonePath")
.Reusable()
.Build();
const CommandLineOption* const OPTION_LIST =
CommandLineOption::Builder::Create()
.WithLongName("list")
.WithDescription("Lists the contents of a zone instead of writing them to the disk.")
.Build();
const CommandLineOption* const OPTION_OUTPUT_FOLDER =
CommandLineOption::Builder::Create()
.WithShortName("o")
.WithLongName("output-folder")
.WithDescription(std::format("Specifies the output folder containing the contents of the unlinked zones. Defaults to \"{}\"", UnlinkerArgs::DEFAULT_OUTPUT_FOLDER))
.WithParameter("outputFolderPath")
.Build();
const CommandLineOption* const OPTION_SEARCH_PATH =
CommandLineOption::Builder::Create()
.WithLongName("search-path")
.WithDescription("Specifies a semi-colon separated list of paths to search for additional game files.")
.WithParameter("searchPathString")
.Build();
const CommandLineOption* const OPTION_IMAGE_FORMAT =
CommandLineOption::Builder::Create()
.WithLongName("image-format")
.WithDescription("Specifies the format of dumped image files. Valid values are: DDS, IWI")
.WithParameter("imageFormatValue")
.Build();
const CommandLineOption* const OPTION_MODEL_FORMAT =
CommandLineOption::Builder::Create()
.WithLongName("model-format")
.WithDescription("Specifies the format of dumped model files. Valid values are: XMODEL_EXPORT, XMODEL_BIN, OBJ, GLTF, GLB")
.WithParameter("modelFormatValue")
.Build();
const CommandLineOption* const OPTION_SKIP_OBJ =
CommandLineOption::Builder::Create()
.WithLongName("skip-obj")
.WithDescription("Skips loading raw obj data.")
.Build();
const CommandLineOption* const OPTION_GDT =
CommandLineOption::Builder::Create()
.WithLongName("gdt")
.WithDescription("Dumps assets in a GDT whenever possible.")
.Build();
const CommandLineOption* const OPTION_EXCLUDE_ASSETS =
CommandLineOption::Builder::Create()
.WithLongName("exclude-assets")
.WithDescription("Specify all asset types that should be excluded.")
.WithParameter("assetTypeList")
.Reusable()
.Build();
const CommandLineOption* const OPTION_INCLUDE_ASSETS =
CommandLineOption::Builder::Create()
.WithLongName("include-assets")
.WithDescription("Specify all asset types that should be included.")
.WithParameter("assetTypeList")
.Reusable()
.Build();
const CommandLineOption* const OPTION_LEGACY_MENUS =
CommandLineOption::Builder::Create()
.WithLongName("legacy-menus")
.WithDescription("Dumps menus with a compatibility mode to work with applications not compatible with the newer dumping mode.")
.Build();
// clang-format on
const CommandLineOption* const COMMAND_LINE_OPTIONS[]{
OPTION_HELP,
OPTION_VERSION,
OPTION_VERBOSE,
OPTION_NO_COLOR,
OPTION_MINIMAL_ZONE_FILE,
OPTION_LOAD,
OPTION_LIST,
OPTION_OUTPUT_FOLDER,
OPTION_SEARCH_PATH,
OPTION_IMAGE_FORMAT,
OPTION_MODEL_FORMAT,
OPTION_SKIP_OBJ,
OPTION_GDT,
OPTION_EXCLUDE_ASSETS,
OPTION_INCLUDE_ASSETS,
OPTION_LEGACY_MENUS,
};
UnlinkerArgs::UnlinkerArgs()
: m_argument_parser(COMMAND_LINE_OPTIONS, std::extent_v<decltype(COMMAND_LINE_OPTIONS)>),
m_zone_pattern(R"(\?zone\?)"),
m_task(ProcessingTask::DUMP),
m_minimal_zone_def(false),
m_asset_type_handling(AssetTypeHandling::EXCLUDE),
m_skip_obj(false),
m_use_gdt(false)
{
}
void UnlinkerArgs::PrintUsage() const
{
UsageInformation usage(m_argument_parser.GetExecutableName());
for (const auto* commandLineOption : COMMAND_LINE_OPTIONS)
{
usage.AddCommandLineOption(commandLineOption);
}
usage.AddArgument("pathToZone");
usage.SetVariableArguments(true);
usage.Print();
}
void UnlinkerArgs::PrintVersion()
{
con::info("OpenAssetTools Unlinker {}", GIT_VERSION);
}
bool UnlinkerArgs::SetImageDumpingMode() const
{
auto specifiedValue = m_argument_parser.GetValueForOption(OPTION_IMAGE_FORMAT);
utils::MakeStringLowerCase(specifiedValue);
if (specifiedValue == "dds")
{
ObjWriting::Configuration.ImageOutputFormat = ObjWriting::Configuration_t::ImageOutputFormat_e::DDS;
return true;
}
if (specifiedValue == "iwi")
{
ObjWriting::Configuration.ImageOutputFormat = ObjWriting::Configuration_t::ImageOutputFormat_e::IWI;
return true;
}
const std::string originalValue = m_argument_parser.GetValueForOption(OPTION_IMAGE_FORMAT);
con::error("Illegal value: \"{}\" is not a valid image output format. Use -? to see usage information.", originalValue);
return false;
}
bool UnlinkerArgs::SetModelDumpingMode() const
{
auto specifiedValue = m_argument_parser.GetValueForOption(OPTION_MODEL_FORMAT);
utils::MakeStringLowerCase(specifiedValue);
if (specifiedValue == "xmodel_export")
{
ObjWriting::Configuration.ModelOutputFormat = ObjWriting::Configuration_t::ModelOutputFormat_e::XMODEL_EXPORT;
return true;
}
if (specifiedValue == "xmodel_bin")
{
ObjWriting::Configuration.ModelOutputFormat = ObjWriting::Configuration_t::ModelOutputFormat_e::XMODEL_BIN;
return true;
}
if (specifiedValue == "obj")
{
ObjWriting::Configuration.ModelOutputFormat = ObjWriting::Configuration_t::ModelOutputFormat_e::OBJ;
return true;
}
if (specifiedValue == "gltf")
{
ObjWriting::Configuration.ModelOutputFormat = ObjWriting::Configuration_t::ModelOutputFormat_e::GLTF;
return true;
}
if (specifiedValue == "glb")
{
ObjWriting::Configuration.ModelOutputFormat = ObjWriting::Configuration_t::ModelOutputFormat_e::GLB;
return true;
}
const std::string originalValue = m_argument_parser.GetValueForOption(OPTION_MODEL_FORMAT);
con::error("Illegal value: \"{}\" is not a valid model output format. Use -? to see usage information.", originalValue);
return false;
}
void UnlinkerArgs::AddSpecifiedAssetType(std::string value)
{
const auto alreadySpecifiedAssetType = m_specified_asset_type_map.find(value);
if (alreadySpecifiedAssetType == m_specified_asset_type_map.end())
{
m_specified_asset_type_map.emplace(std::make_pair(value, m_specified_asset_types.size()));
m_specified_asset_types.emplace_back(std::move(value));
}
}
void UnlinkerArgs::ParseCommaSeparatedAssetTypeString(const std::string& input)
{
auto currentPos = 0uz;
size_t endPos;
std::string lowerInput(input);
utils::MakeStringLowerCase(lowerInput);
while (currentPos < lowerInput.size() && (endPos = lowerInput.find_first_of(',', currentPos)) != std::string::npos)
{
AddSpecifiedAssetType(std::string(lowerInput, currentPos, endPos - currentPos));
currentPos = endPos + 1;
}
if (currentPos < lowerInput.size())
AddSpecifiedAssetType(std::string(lowerInput, currentPos, lowerInput.size() - currentPos));
}
bool UnlinkerArgs::ParseArgs(const int argc, const char** argv, bool& shouldContinue)
{
shouldContinue = true;
if (!m_argument_parser.ParseArguments(argc, argv))
{
PrintUsage();
return false;
}
// Check if the user requested help
if (m_argument_parser.IsOptionSpecified(OPTION_HELP))
{
PrintUsage();
shouldContinue = false;
return true;
}
// Check if the user wants to see the version
if (m_argument_parser.IsOptionSpecified(OPTION_VERSION))
{
PrintVersion();
shouldContinue = false;
return true;
}
m_zones_to_unlink = m_argument_parser.GetArguments();
const size_t zoneCount = m_zones_to_unlink.size();
if (zoneCount < 1)
{
// No zones to load specified...
PrintUsage();
return false;
}
// -v; --verbose
if (m_argument_parser.IsOptionSpecified(OPTION_VERBOSE))
con::set_log_level(con::LogLevel::DEBUG);
else
con::set_log_level(con::LogLevel::INFO);
// --no-color
con::set_use_color(!m_argument_parser.IsOptionSpecified(OPTION_NO_COLOR));
// -min; --minimal-zone
m_minimal_zone_def = m_argument_parser.IsOptionSpecified(OPTION_MINIMAL_ZONE_FILE);
// -l; --load
if (m_argument_parser.IsOptionSpecified(OPTION_LOAD))
m_zones_to_load = m_argument_parser.GetParametersForOption(OPTION_LOAD);
// --list
if (m_argument_parser.IsOptionSpecified(OPTION_LIST))
m_task = ProcessingTask::LIST;
// -o; --output-folder
if (m_argument_parser.IsOptionSpecified(OPTION_OUTPUT_FOLDER))
m_output_folder = m_argument_parser.GetValueForOption(OPTION_OUTPUT_FOLDER);
else
m_output_folder = DEFAULT_OUTPUT_FOLDER;
// --search-path
if (m_argument_parser.IsOptionSpecified(OPTION_SEARCH_PATH))
{
if (!FileUtils::ParsePathsString(m_argument_parser.GetValueForOption(OPTION_SEARCH_PATH), m_user_search_paths))
{
return false;
}
}
// --image-format
if (m_argument_parser.IsOptionSpecified(OPTION_IMAGE_FORMAT))
{
if (!SetImageDumpingMode())
{
return false;
}
}
// --model-format
if (m_argument_parser.IsOptionSpecified(OPTION_MODEL_FORMAT))
{
if (!SetModelDumpingMode())
{
return false;
}
}
// --skip-obj
m_skip_obj = m_argument_parser.IsOptionSpecified(OPTION_SKIP_OBJ);
// --gdt
m_use_gdt = m_argument_parser.IsOptionSpecified(OPTION_GDT);
// --exclude-assets
// --include-assets
if (m_argument_parser.IsOptionSpecified(OPTION_EXCLUDE_ASSETS) && m_argument_parser.IsOptionSpecified(OPTION_INCLUDE_ASSETS))
{
con::error("You can only asset types to either exclude or include, not both");
return false;
}
if (m_argument_parser.IsOptionSpecified(OPTION_EXCLUDE_ASSETS))
{
m_asset_type_handling = AssetTypeHandling::EXCLUDE;
for (const auto& exclude : m_argument_parser.GetParametersForOption(OPTION_EXCLUDE_ASSETS))
ParseCommaSeparatedAssetTypeString(exclude);
}
else if (m_argument_parser.IsOptionSpecified(OPTION_INCLUDE_ASSETS))
{
m_asset_type_handling = AssetTypeHandling::INCLUDE;
for (const auto& include : m_argument_parser.GetParametersForOption(OPTION_INCLUDE_ASSETS))
ParseCommaSeparatedAssetTypeString(include);
}
// --legacy-menus
if (m_argument_parser.IsOptionSpecified(OPTION_LEGACY_MENUS))
ObjWriting::Configuration.MenuLegacyMode = true;
return true;
}
std::string UnlinkerArgs::GetOutputFolderPathForZone(const Zone& zone) const
{
return std::regex_replace(m_output_folder, m_zone_pattern, zone.m_name);
}

View File

@@ -0,0 +1,70 @@
#pragma once
#include "Utils/Arguments/ArgumentParser.h"
#include "Zone/Zone.h"
#include <cstdint>
#include <regex>
#include <set>
#include <string>
#include <unordered_map>
#include <vector>
class UnlinkerArgs
{
public:
static constexpr const char* DEFAULT_OUTPUT_FOLDER = "zone_dump/zone_raw/?zone?";
private:
ArgumentParser m_argument_parser;
std::regex m_zone_pattern;
/**
* \brief Prints a command line usage help text for the Unlinker tool to stdout.
*/
void PrintUsage() const;
static void PrintVersion();
bool SetImageDumpingMode() const;
bool SetModelDumpingMode() const;
void AddSpecifiedAssetType(std::string value);
void ParseCommaSeparatedAssetTypeString(const std::string& input);
public:
enum class ProcessingTask
{
DUMP,
LIST
};
enum class AssetTypeHandling : std::uint8_t
{
EXCLUDE,
INCLUDE
};
std::vector<std::string> m_zones_to_load;
std::vector<std::string> m_zones_to_unlink;
std::set<std::string> m_user_search_paths;
ProcessingTask m_task;
std::string m_output_folder;
bool m_minimal_zone_def;
std::vector<std::string> m_specified_asset_types;
std::unordered_map<std::string, size_t> m_specified_asset_type_map;
AssetTypeHandling m_asset_type_handling;
bool m_skip_obj;
bool m_use_gdt;
UnlinkerArgs();
bool ParseArgs(int argc, const char** argv, bool& shouldContinue);
/**
* \brief Converts the output path specified by command line arguments to a path applies for the specified zone.
* \param zone The zone to resolve the path input for.
* \return An output path for the zone based on the user input.
*/
std::string GetOutputFolderPathForZone(const Zone& zone) const;
};

View File

@@ -0,0 +1,91 @@
#include "UnlinkerPaths.h"
#include "SearchPath/IWD.h"
#include "SearchPath/SearchPathFilesystem.h"
#include "Utils/Logging/Log.h"
#include "Utils/StringUtils.h"
#include <filesystem>
#include <format>
#include <iostream>
namespace fs = std::filesystem;
bool UnlinkerPaths::LoadUserPaths(const UnlinkerArgs& args)
{
for (const auto& path : args.m_user_search_paths)
{
auto absolutePath = fs::absolute(path);
if (!fs::is_directory(absolutePath))
{
con::error("Could not find directory of search path: \"{}\"", path);
return false;
}
auto searchPathName = absolutePath.string();
m_user_paths.CommitSearchPath(std::make_unique<SearchPathFilesystem>(searchPathName));
m_specified_user_paths.emplace(std::move(searchPathName));
std::filesystem::directory_iterator iterator(absolutePath);
const auto end = fs::end(iterator);
for (auto i = fs::begin(iterator); i != end; ++i)
{
if (!i->is_regular_file())
continue;
auto extension = i->path().extension().string();
utils::MakeStringLowerCase(extension);
if (extension == ".iwd")
{
auto iwd = iwd::LoadFromFile(i->path().string());
if (iwd)
m_user_paths.CommitSearchPath(std::move(iwd));
}
}
}
con::info("{} SearchPaths{}", m_specified_user_paths.size(), !m_specified_user_paths.empty() ? ":" : "");
for (const auto& absoluteSearchPath : m_specified_user_paths)
con::info(" \"{}\"", absoluteSearchPath);
if (!m_specified_user_paths.empty())
con::info("");
return true;
}
std::unique_ptr<ISearchPath> UnlinkerPaths::GetSearchPathsForZone(const std::string& zonePath)
{
const auto absoluteZoneDirectory = fs::absolute(std::filesystem::path(zonePath).remove_filename());
const auto absoluteZoneDirectoryString = absoluteZoneDirectory.string();
if (m_last_zone_path != absoluteZoneDirectoryString)
{
m_last_zone_path = absoluteZoneDirectoryString;
m_last_zone_search_paths = SearchPaths();
m_last_zone_search_paths.CommitSearchPath(std::make_unique<SearchPathFilesystem>(absoluteZoneDirectoryString));
std::filesystem::directory_iterator iterator(absoluteZoneDirectory);
const auto end = fs::end(iterator);
for (auto i = fs::begin(iterator); i != end; ++i)
{
if (!i->is_regular_file())
continue;
auto extension = i->path().extension().string();
utils::MakeStringLowerCase(extension);
if (extension == ".iwd")
{
auto iwd = iwd::LoadFromFile(i->path().string());
if (iwd)
m_last_zone_search_paths.CommitSearchPath(std::move(iwd));
}
}
}
auto result = std::make_unique<SearchPaths>();
result->IncludeSearchPath(&m_last_zone_search_paths);
result->IncludeSearchPath(&m_user_paths);
return result;
}

View File

@@ -0,0 +1,21 @@
#pragma once
#include "SearchPath/SearchPaths.h"
#include "UnlinkerArgs.h"
#include <string>
#include <unordered_set>
class UnlinkerPaths
{
public:
bool LoadUserPaths(const UnlinkerArgs& args);
std::unique_ptr<ISearchPath> GetSearchPathsForZone(const std::string& zonePath);
private:
std::string m_last_zone_path;
SearchPaths m_last_zone_search_paths;
std::unordered_set<std::string> m_specified_user_paths;
SearchPaths m_user_paths;
};