Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
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
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
8 changes: 8 additions & 0 deletions build.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
BINARY_PATH = r"build\Release\bin\codxe.xex"
STAGING_DIR = r"build\staging"
RESOURCES_PATH = r"resources"
DATA_PATH = r"_codxe"
VERSION_HEADER_PATH = r"build\Release\obj\git_version.h"


Expand Down Expand Up @@ -56,6 +57,13 @@ def read_git_version():
print(f"Resources directory not found at {RESOURCES_PATH}")
exit(1)

if os.path.isdir(DATA_PATH):
shutil.copytree(DATA_PATH, os.path.join(STAGING_DIR, "_codxe"), dirs_exist_ok=True)
print("CoD Xe data directory copied successfully")
else:
print(f"CoD Xe data directory not found at {DATA_PATH}")
exit(1)

print("Copying binary to all title ID directories...")
plugins_dir = os.path.join(STAGING_DIR, "xenia", "plugins")
if os.path.exists(plugins_dir):
Expand Down
2 changes: 2 additions & 0 deletions codxe.vcxproj
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
<ClCompile Include="src\common\brush_collision_tracker.cpp" />
<ClCompile Include="src\common\config.cpp" />
<ClCompile Include="src\common\endian.cpp" />
<ClCompile Include="src\common\game_id.cpp" />
<ClCompile Include="src\common\gsc_loader.cpp" />
<ClCompile Include="src\common\map_ents.cpp" />
<ClCompile Include="src\common\ppc.cpp" />
Expand Down Expand Up @@ -235,6 +236,7 @@
<ClInclude Include="src\common\brush_collision_tracker.h" />
<ClInclude Include="src\common\config.h" />
<ClInclude Include="src\common\endian.h" />
<ClInclude Include="src\common\game_id.h" />
<ClInclude Include="src\common\gsc_loader.h" />
<ClInclude Include="src\common\map_ents.h" />
<ClInclude Include="src\common\ppc.h" />
Expand Down
29 changes: 18 additions & 11 deletions docs/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,13 @@ The raw GSC loader enables loading `.gsc` scripts directly from the mod director

### Setup

Create a `_codxe\mods` folder in your game directory, and place a `codxe.json` in `_codxe` to define which mod is active.
Create a `_codxe\<gameID>\mods` folder in your game directory, and place a `codxe.json` in `_codxe\<gameID>` to define which mod is active. Use `iw2`, `iw3`, `iw4`, `iw5`, `t4`, `t5`, or `qos` for the game ID.

The repository and release archive contain one top-level [`_codxe`](/_codxe) folder with a subdirectory for each game. Copy that folder into your game directory or a USB/HDD root, preserving `_codxe\<gameID>\...`. All supported games that use CoD Xe data share this layout. Title updates and Xenia files remain separate from the game data.

CoD Xe selects one data root at startup. If `_codxe\<gameID>` exists, all config, mod, fastfile, usermap, internal asset, dump, and player stats paths use that root. Missing files never fall back to the legacy `_codxe` root. The old layout remains supported when the nested directory does not exist. Keep all data for a game together when migrating.

On Xbox hardware, when neither local directory exists, CoD Xe searches `Usb0:\_codxe\<gameID>` through `Usb4:\_codxe\<gameID>`, then `Hdd1:\_codxe\<gameID>`. The first existing directory becomes the only active root for reads and writes. USB/HDD roots only accept the nested layout. If no existing root is found, new files use the local `_codxe\<gameID>` directory. Xenia uses the local directories.

In `codxe.json`, set `"active_mod"` to the name of your mod folder:

Expand All @@ -19,22 +25,23 @@ In `codxe.json`, set `"active_mod"` to the name of your mod folder:
Once configured, the engine will redirect script lookups to:

```text
game:\_codxe\mods\my_mod\
game:\_codxe\<gameID>\mods\my_mod\
```

Example tree structure:

```text
game:.
`-- _codxe
|-- codxe.json
`-- mods
`-- my_mod
`-- maps
`-- mp
`-- gametypes
|-- _callbacksetup.gsc
`-- custom_logic.gsc
`-- <gameID>
|-- codxe.json
`-- mods
`-- my_mod
`-- maps
`-- mp
`-- gametypes
|-- _callbacksetup.gsc
`-- custom_logic.gsc
```

The underscore keeps the folder at the top of file listings and separates CoD Xe system files from the game's core content.
Expand All @@ -46,7 +53,7 @@ The loader operates using a virtual filesystem. All `.gsc` scripts, whether load
For example:

- A file in `common_mp.ff/maps/mp/gametypes/_callbacksetup.gsc`
- Can be overridden by `game:\_codxe\mods\my_mod\maps\mp\gametypes\_callbacksetup.gsc`
- Can be overridden by `game:\_codxe\<gameID>\mods\my_mod\maps\mp\gametypes\_callbacksetup.gsc`

Your version will take precedence over the copy embedded in the original fastfile. You can also add new scripts and call them from overridden entry points such as `_callbacksetup.gsc`.

Expand Down
2 changes: 1 addition & 1 deletion docs/t4.md
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ self.god = false;

## Example Mods

Example T4 mods are available under [`resources/t4/_codxe/mods`](/resources/t4/_codxe/mods):
Example T4 mods are available under [`_codxe/t4/mods`](/_codxe/t4/mods):

- `example` - Basic script examples for singleplayer and multiplayer.
- `codjumper` - Codjumper-oriented scripts and menu examples.
112 changes: 50 additions & 62 deletions src/common/config.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -9,35 +9,18 @@
#define INVALID_FILE_SIZE ((DWORD) - 1)
#endif

const char *CONFIG_PATH = "game:\\_codxe\\codxe.json";
const char *MOD_DIR = "game:\\_codxe\\mods";
const char *DUMP_DIR = "game:\\_codxe\\dump";

// Default values
char Config::active_mod[MAX_PATH] = {};
bool Config::dump_rawfile = false;
bool Config::dump_map_ents = false;
char Config::mod_base_path[MAX_PATH] = {};
std::string Config::data_root;
bool Config::shared_layout_enabled = false;
std::vector<std::string> Config::mounted_links;

namespace
{
const char *CONFIG_DEVICE_LINK_NAME = "codxe:";
const char *CONFIG_DEVICE_LINK_PATH = "codxe:\\";

const char *GetGameDirectoryName(Config::GameId gameId)
{
switch (gameId)
{
case Config::GAME_T4:
return "t4";
case Config::GAME_NONE:
default:
return nullptr;
}
}
const char *const DEVICE_LINK_NAMESPACES[] = {"\\System??\\", "\\??\\"};

bool StartsWith(const std::string &value, const char *prefix)
{
Expand Down Expand Up @@ -65,30 +48,41 @@ bool TryGetDeviceRoot(const std::string &path, std::string &device_root)
return true;
}

bool MountDevice(const char *link_name, const char *device_path)
void UnmountDevice(const char *link_name)
{
const DWORD process_type = KeGetCurrentProcessType();
const std::string link_path = std::string(process_type == PROC_TYPE_SYSTEM ? "\\System??\\" : "\\??\\") + link_name;
for (size_t i = 0; i < ARRAYSIZE(DEVICE_LINK_NAMESPACES); ++i)
{
const std::string link_path = std::string(DEVICE_LINK_NAMESPACES[i]) + link_name;
STRING link = {};
RtlInitAnsiString(&link, link_path.c_str());
ObDeleteSymbolicLink(&link);
}
}

STRING link = {};
bool MountDevice(const char *link_name, const char *device_path)
{
STRING device = {};
RtlInitAnsiString(&link, link_path.c_str());
RtlInitAnsiString(&device, device_path);

const NTSTATUS delete_status = ObDeleteSymbolicLink(&link);
const NTSTATUS create_status = ObCreateSymbolicLink(&link, &device);
UNREFERENCED_PARAMETER(delete_status);

return NT_SUCCESS(create_status);
}
// Plugin setup runs in the system process; game reads and writes run in the title process.
// DOS device links are namespace-specific, so both must point to the same selected device.
for (size_t i = 0; i < ARRAYSIZE(DEVICE_LINK_NAMESPACES); ++i)
{
const std::string link_path = std::string(DEVICE_LINK_NAMESPACES[i]) + link_name;
STRING link = {};
RtlInitAnsiString(&link, link_path.c_str());
ObDeleteSymbolicLink(&link);
const NTSTATUS status = ObCreateSymbolicLink(&link, &device);
if (!NT_SUCCESS(status))
{
DbgPrint("[codxe][Config] Failed to mount %s -> %s: status=0x%08X\n", link_path.c_str(), device_path,
status);
UnmountDevice(link_name);
return false;
}
}

void UnmountDevice(const char *link_name)
{
const DWORD process_type = KeGetCurrentProcessType();
const std::string link_path = std::string(process_type == PROC_TYPE_SYSTEM ? "\\System??\\" : "\\??\\") + link_name;
STRING link = {};
RtlInitAnsiString(&link, link_path.c_str());
ObDeleteSymbolicLink(&link);
return true;
}

bool BuildMountedConfigPath(const std::string &device_config_path, std::string &mounted_config_path)
Expand Down Expand Up @@ -166,14 +160,12 @@ bool ReadFileToString(const char *path, std::string &outString)
return true;
}

Config::Config(GameId gameId)
Config::Config(GameId::Type gameId)
{
data_root = "game:\\_codxe";
mounted_links.clear();
const char *gameDirectoryName = GetGameDirectoryName(gameId);
shared_layout_enabled = gameDirectoryName != nullptr;

std::string config_path = CONFIG_PATH;
const char *gameDirectoryName = GameId::ToString(gameId);
std::string config_path = filesystem::JoinPath(data_root.c_str(), "codxe.json");

if (xbox::GetEnvironment() != xbox::ENVIRONMENT_XENIA)
{
Expand All @@ -192,7 +184,7 @@ Config::Config(GameId gameId)
if (slash != std::string::npos)
{
// On hardware Config is constructed before game: is retargeted to the new title.
// Only the config file load needs this early executable-relative path.
// Root selection and config loading need this early executable-relative path.
config_path = full_path.substr(0, slash) + "\\_codxe\\codxe.json";

std::string mounted_config_path;
Expand All @@ -207,7 +199,7 @@ Config::Config(GameId gameId)
}
}

if (shared_layout_enabled)
if (gameDirectoryName != nullptr)
{
// Config loads before game: is retargeted on hardware, so inspect the
// executable-relative path while choosing the single active root.
Expand All @@ -225,7 +217,9 @@ Config::Config(GameId gameId)
}
else
{
data_root.clear();
// If no existing root is found, new files use the local nested layout.
data_root = std::string("game:\\_codxe\\") + gameDirectoryName;
config_path = filesystem::JoinPath(nested_local_directory.c_str(), "codxe.json");
if (xbox::GetEnvironment() != xbox::ENVIRONMENT_XENIA)
{
// Only the game directory accepts the legacy layout.
Expand Down Expand Up @@ -275,11 +269,9 @@ Config::~Config()
{
// Reset to defaults on cleanup
active_mod[0] = '\0';
mod_base_path[0] = '\0';
dump_rawfile = false;
dump_map_ents = false;
data_root.clear();
shared_layout_enabled = false;
for (size_t i = 0; i < mounted_links.size(); ++i)
UnmountDevice(mounted_links[i].c_str());
mounted_links.clear();
Expand Down Expand Up @@ -341,13 +333,6 @@ bool Config::LoadFromJson(const char *jsonBuffer, DWORD bufferSize)
DbgPrint(" Dump Raw Scripts: %s\n", dump_rawfile ? "true" : "false");
DbgPrint(" Dump Map Entities: %s\n", dump_map_ents ? "true" : "false");

if (active_mod[0] != '\0')
{
// Config loads before game: is guaranteed to point at the new executable root on hardware.
// Keep the intended path and let later game-lifecycle file accesses validate it naturally.
_snprintf_s(mod_base_path, ARRAYSIZE(mod_base_path), _TRUNCATE, "%s\\%s", MOD_DIR, active_mod);
}

return true;
}

Expand All @@ -373,24 +358,27 @@ const char *Config::GetActiveMod()

std::string Config::ResolveModPath(const char *relativePath)
{
if (!relativePath || !*relativePath || !mod_base_path[0])
if (!relativePath || !*relativePath || !active_mod[0])
return std::string();

if (shared_layout_enabled)
{
const std::string mod_path = filesystem::JoinPath("mods", active_mod);
return ResolveDataPath(filesystem::JoinPath(mod_path.c_str(), relativePath).c_str());
}
const std::string mod_path = filesystem::JoinPath("mods", active_mod);
return BuildDataPath(filesystem::JoinPath(mod_path.c_str(), relativePath).c_str());
}

std::string Config::BuildDataPath(const char *relativePath)
{
if (!relativePath || data_root.empty())
return std::string();

return filesystem::JoinPath(mod_base_path, relativePath);
return filesystem::JoinPath(data_root.c_str(), relativePath);
}

std::string Config::ResolveDataPath(const char *relativePath)
{
if (!relativePath || !*relativePath || data_root.empty())
return std::string();

const std::string path = filesystem::JoinPath(data_root.c_str(), relativePath);
const std::string path = BuildDataPath(relativePath);
return filesystem::FileExists(path.c_str()) ? path : std::string();
}

Expand All @@ -399,7 +387,7 @@ std::string Config::ResolveDataDirectory(const char *relativePath)
if (!relativePath || !*relativePath || data_root.empty())
return std::string();

const std::string path = filesystem::JoinPath(data_root.c_str(), relativePath);
const std::string path = BuildDataPath(relativePath);
return filesystem::DirectoryExists(path.c_str()) ? path : std::string();
}

Expand Down
17 changes: 4 additions & 13 deletions src/common/config.h
Original file line number Diff line number Diff line change
@@ -1,39 +1,30 @@
#pragma once

#include "pch.h"

extern const char *CONFIG_PATH;
extern const char *MOD_DIR;
extern const char *DUMP_DIR;
#include "game_id.h"

bool ReadFileToString(const char *path, std::string &outString);

class Config : public Module
{
public:
enum GameId
{
GAME_NONE,
GAME_T4,
};

explicit Config(GameId gameId = GAME_NONE);
explicit Config(GameId::Type gameId);
~Config();

static bool dump_rawfile;
static bool dump_map_ents;

static const char *GetActiveMod();
static std::string ResolveModPath(const char *relativePath);
// Build paths within the root selected at startup, including files that do not exist yet.
static std::string BuildDataPath(const char *relativePath);
static std::string ResolveDataPath(const char *relativePath);
static std::string ResolveDataDirectory(const char *relativePath);
static std::string ResolveDataPathForGameFile(const char *relativePath);

private:
static char active_mod[MAX_PATH];
static char mod_base_path[MAX_PATH];
static std::string data_root;
static bool shared_layout_enabled;
static std::vector<std::string> mounted_links;

bool LoadFromJson(const char *jsonBuffer, DWORD bufferSize);
Expand Down
28 changes: 28 additions & 0 deletions src/common/game_id.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#include "pch.h"
#include "game_id.h"

namespace GameId
{
const char *ToString(Type gameId)
{
switch (gameId)
{
case IW2:
return "iw2";
case IW3:
return "iw3";
case IW4:
return "iw4";
case IW5:
return "iw5";
case T4:
return "t4";
case T5:
return "t5";
case QOS:
return "qos";
default:
return nullptr;
}
}
} // namespace GameId
Loading
Loading