diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ec00e3114..7dcb55484 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -164,6 +164,7 @@ add_executable(SRB2SDL2 MACOSX_BUNDLE WIN32 k_bans.cpp k_endcam.cpp k_credits.cpp + k_modinfo.cpp music.cpp music_manager.cpp sanitize.cpp diff --git a/src/k_modinfo.cpp b/src/k_modinfo.cpp new file mode 100644 index 000000000..9b3ebcf78 --- /dev/null +++ b/src/k_modinfo.cpp @@ -0,0 +1,210 @@ +// DR. ROBOTNIK'S RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2024 by Sally "TehRealSalt" Cochenour +// Copyright (C) 2024 by Kart Krew +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- +/// \file k_modinfo.cpp +/// \brief Mod metadata + +#include "k_modinfo.h" + +#include +#include +#include +#include +#include + +#include "w_wad.h" +#include "z_zone.h" + +using nlohmann::json; + +void mod_metadata_t::init_defaults(void) +{ + // Initialize default fields. + _game_version = 2; // Defaults to the latest version + _game_subversion = 3; // that did not support MODINFO lumps. +} + +bool mod_metadata_t::parse_info_json(const char *contents, size_t contents_len) +{ + if (contents_len == 0) + { + CONS_Alert(CONS_ERROR, "could not parse MODINFO; contents are empty\n"); + return false; + } + + json info_obj = json::parse(contents, contents + static_cast(contents_len)); + if (info_obj.is_object() == false) + { + CONS_Alert(CONS_ERROR, "could not parse MODINFO; is not a JSON object\n"); + return false; + } + + _name = info_obj.value("name", _name); + _author = info_obj.value("author", _author); + _description = info_obj.value("description", _description); + _info_url = info_obj.value("info_url", _info_url); + _version = info_obj.value("version", _version); + + std::string game_version_str = info_obj.value("game_version", ""); + if (game_version_str.empty() == false) + { + int game_version_int = 0, game_subversion_int = 0; + int result = sscanf(game_version_str.c_str(), "%d.%d", &game_version_int, &game_subversion_int); + + if (result >= 1 && game_version_int >= 0 && game_version_int <= UINT16_MAX) + { + _game_version = static_cast(game_version_int); + _game_subversion = 0; // Just inputting "2" should be the same as "2.0" + } + + if (result >= 2 && game_subversion_int >= 0 && game_subversion_int <= UINT16_MAX) + { + _game_subversion = static_cast(game_subversion_int); + } + } + + return true; +} + +// Create mod metadata from a WAD file ID. +// There is the possibility of allocating mod_metadata_t +// for an unloaded file for the addons menu, which should +// be a different constructor. +mod_metadata_t::mod_metadata_t(size_t wad_id) +{ + init_defaults(); + + // Default to the WAD's name + _name = wadfiles[wad_id]->filename; + + lumpnum_t lump_index; + lumpinfo_t *lump_p = wadfiles[wad_id]->lumpinfo; + + lumpnum_t icon_lump = LUMPERROR; + lumpnum_t info_lump = LUMPERROR; + + for (lump_index = 0; lump_index < wadfiles[wad_id]->numlumps; lump_index++, lump_p++) + { + if (info_lump == LUMPERROR && memcmp(lump_p->name, "MODINFO", 8) == 0) + { + info_lump = lump_index; + } + else if (icon_lump == LUMPERROR && memcmp(lump_p->name, "MODICON", 8) == 0) + { + icon_lump = lump_index; + } + + if (info_lump != LUMPERROR && icon_lump != LUMPERROR) + { + // Found all lumps, exit early. + break; + } + } + + if (info_lump != LUMPERROR) + { + // Parse info JSON + size_t info_lump_len = W_LumpLengthPwad(wad_id, info_lump); + char *info_contents = static_cast( W_CacheLumpNumPwad(wad_id, info_lump, PU_STATIC) ); + + parse_info_json(info_contents, info_lump_len); + + Z_Free(info_contents); + } + + if (icon_lump != LUMPERROR) + { + // We have an icon lump. Cache it! + _icon = static_cast( W_CachePatchNumPwad(wad_id, icon_lump, PU_STATIC) ); + } + + // TODO: Make incompatibile WADs exit early and unload? + // It would require that we pass in the temporary lumpinfo and handle. + // It doesn't look hard, just very tedious, and I can't be assed rn. + mod_compat_e is_compatible = Compatible(); + switch (is_compatible) + { + case MOD_INCOMPATIBLE_FUTURE: + { + CONS_Alert( + CONS_ERROR, + "Mod '%s' was designed for a newer version of Ring Racers (mod is for v%d.%d, you have v%d.%d). Update your copy!\n", + _name.c_str(), + _game_version, _game_subversion, + VERSION, SUBVERSION + ); + break; + } + case MOD_INCOMPATIBLE_PAST: + { + CONS_Alert( + CONS_ERROR, + "Mod '%s' was designed for a backwards-incompatible version of Ring Racers (mod is for v%d.%d, you have v%d.%d). This mod will need updated before it can be used.\n", + _name.c_str(), + _game_version, _game_subversion, + VERSION, SUBVERSION + ); + break; + } + default: + { + // Mod is compatible! + break; + } + } +} + +// +// C interface functions +// + +patch_t *ModMetadata_GetIcon(mod_metadata_t *meta) +{ + return meta->icon(); +} + +UINT16 ModMetadata_GetGameVersion(mod_metadata_t *meta) +{ + return meta->game_version(); +} + +UINT16 ModMetadata_GetGameSubVersion(mod_metadata_t *meta) +{ + return meta->game_subversion(); +} + +char *ModMetadata_GetName(mod_metadata_t *meta) +{ + return Z_StrDup( meta->name().c_str() ); +} + +char *ModMetadata_GetAuthor(mod_metadata_t *meta) +{ + return Z_StrDup( meta->author().c_str() ); +} + +char *ModMetadata_GetDescription(mod_metadata_t *meta) +{ + return Z_StrDup( meta->description().c_str() ); +} + +char *ModMetadata_GetInfoURL(mod_metadata_t *meta) +{ + return Z_StrDup( meta->info_url().c_str() ); +} + +char *ModMetadata_GetVersion(mod_metadata_t *meta) +{ + return Z_StrDup( meta->version().c_str() ); +} + +mod_compat_e ModMetadata_Compatible(mod_metadata_t *meta) +{ + return meta->Compatible(); +} diff --git a/src/k_modinfo.h b/src/k_modinfo.h new file mode 100644 index 000000000..5b6e38d6a --- /dev/null +++ b/src/k_modinfo.h @@ -0,0 +1,125 @@ +// DR. ROBOTNIK'S RING RACERS +//----------------------------------------------------------------------------- +// Copyright (C) 2024 by Sally "TehRealSalt" Cochenour +// Copyright (C) 2024 by Kart Krew +// +// This program is free software distributed under the +// terms of the GNU General Public License, version 2. +// See the 'LICENSE' file for more details. +//----------------------------------------------------------------------------- +/// \file k_modinfo.h +/// \brief Mod metadata + +#ifndef __K_MODINFO_H__ +#define __K_MODINFO_H__ + +#include "typedef.h" +#include "doomtype.h" +#include "doomdef.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum +{ + MOD_COMPATIBLE = 0, // Fully compatible. + MOD_INCOMPATIBLE_FUTURE, // For a future version. + MOD_INCOMPATIBLE_PAST // For a way way too old version. +} mod_compat_e; + +#ifdef __cplusplus +} // extern "C" +#endif + +#ifdef __cplusplus + +#include + +class mod_metadata_t +{ +private: + patch_t *_icon; + UINT16 _game_version; + UINT16 _game_subversion; + + std::string _name; + std::string _author; + std::string _description; + std::string _info_url; + std::string _version; + + void init_defaults(void); + bool parse_info_json(const char *contents, size_t contents_len); + +public: + mod_metadata_t(size_t wad_id); + + patch_t *icon() const { return _icon; } + UINT16 game_version() const { return _game_version; } + UINT16 game_subversion() const { return _game_subversion; } + + std::string name() const { return _name; } + std::string author() const { return _author; } + std::string description() const { return _description; } + std::string info_url() const { return _info_url; } + std::string version() const { return _version; } + + mod_compat_e Compatible() const + { +#if (VERSION == 0 && SUBVERSION == 0) + // DEVELOP builds don't have any relevant version info. + // Just disable the checks entirely, you probably + // know what you're doing. + return MOD_COMPATIBLE; +#else + if (_game_version > VERSION || (_game_version == VERSION && _game_subversion > SUBVERSION)) + { + // This mod might use features that don't exist + // in earlier versions, so tell the user to + // update their game to play with this mod. + return MOD_INCOMPATIBLE_FUTURE; + } + + if (_game_version < VERSION) + { + // Whenever VERSION is incremented, it's because mod + // backwards compatibility has been broken beyond + // repair. This mod is too old to support! + return MOD_INCOMPATIBLE_PAST; + } + + return MOD_COMPATIBLE; +#endif + } +}; + +#else + +// C compatibility interface +struct mod_metadata_t; +typedef struct mod_metadata_t mod_metadata_t; + +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +patch_t *ModMetadata_GetIcon(mod_metadata_t *meta); +UINT16 ModMetadata_GetGameVersion(mod_metadata_t *meta); +UINT16 ModMetadata_GetGameSubVersion(mod_metadata_t *meta); + +char *ModMetadata_GetName(mod_metadata_t *meta); +char *ModMetadata_GetAuthor(mod_metadata_t *meta); +char *ModMetadata_GetDescription(mod_metadata_t *meta); +char *ModMetadata_GetInfoURL(mod_metadata_t *meta); +char *ModMetadata_GetVersion(mod_metadata_t *meta); + +mod_compat_e ModMetadata_Compatible(mod_metadata_t *meta); + +#ifdef __cplusplus +} // extern "C" +#endif + +#endif // __K_MODINFO_H__ diff --git a/src/w_wad.cpp b/src/w_wad.cpp index 4a2c11ed3..8b1c0ccc9 100644 --- a/src/w_wad.cpp +++ b/src/w_wad.cpp @@ -993,6 +993,28 @@ UINT16 W_InitFile(const char *filename, boolean mainfile, boolean startup, const wadfiles[numwadfiles] = wadfile; numwadfiles++; // must come BEFORE W_LoadDehackedLumps, so any addfile called by COM_BufInsertText called by Lua doesn't overwrite what we just loaded + // + // fill out metadata + // + if (mainfile == false) // main files do not need a MODINFO + { + wadfile->metadata = new mod_metadata_t(numwadfiles - 1); + + CONS_Printf( + "== %s (version %s) - by %s ==\n" + "%s\n" + "More @ %s\n" + "[ DESIGNED FOR RING RACERS %d.%d ]\n", + wadfile->metadata->name().c_str(), + wadfile->metadata->version().c_str(), + wadfile->metadata->author().c_str(), + wadfile->metadata->description().c_str(), + wadfile->metadata->info_url().c_str(), + wadfile->metadata->game_version(), + wadfile->metadata->game_subversion() + ); + } + #ifdef HWRENDER // Read shaders from file if (rendermode == render_opengl && (vid.glstate == VID_GL_LIBRARY_LOADED)) @@ -2432,6 +2454,9 @@ int W_VerifyNMUSlumps(const char *filename, boolean exit_on_error) {"TLG_", 4}, // Generic button legends + {"MODINFO", 7}, // Addon metadata + {"MODICON", 7}, // Addon icon + #ifdef HWRENDER {"SHADERS", 7}, {"SH_", 3}, diff --git a/src/w_wad.h b/src/w_wad.h index b834e31a8..b56e05248 100644 --- a/src/w_wad.h +++ b/src/w_wad.h @@ -19,6 +19,8 @@ #include "hardware/hw_data.h" #endif +#include "k_modinfo.h" + #ifdef __cplusplus extern "C" { #endif @@ -130,6 +132,8 @@ struct wadfile_t UINT8 md5sum[16]; boolean important; // also network - !W_VerifyNMUSlumps + + mod_metadata_t *metadata; }; #define WADFILENUM(lumpnum) (UINT16)((lumpnum)>>16) // wad flumpnum>>16) // wad file number in upper word