Skip to content

Commit

Permalink
texture mods
Browse files Browse the repository at this point in the history
  • Loading branch information
Dregu committed Jun 16, 2024
1 parent 03bc81b commit b06b4a7
Show file tree
Hide file tree
Showing 6 changed files with 202 additions and 86 deletions.
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ add_library(dll SHARED
search.cpp search.h
virtual_table.cpp virtual_table.h
version.cpp version.h
image.cpp image.h
ghidra_byte_string.h
tokenize.h
logger.h
Expand Down
33 changes: 24 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,26 +47,41 @@ Build with cmake or get the [Autobuild](https://github.com/Dregu/maxwell/release
- Ctrl+Tab to toggle
- Tab to skip one frame
- Rudimentary runtime level editing
- WIP
- Change water level
- Middle click to pick tile from room
- Left click to place tile
- Mouse4 to mark tiles destroyed
- Hold shift for background layer or fixing destroyed tiles
- Search for tiles by ID in the current map
- Highlight and mass replace found tiles
- Import map files exported from the [editor](https://github.com/Redcrafter/Animal-Well-editor)
- Static tiles will update instantly, dynamic tiles need a room reload
- Very WIP
- Import map files exported from the [level editor](https://github.com/Redcrafter/Animal-Well-editor) at runtime
- Static tiles will update instantly, dynamic tiles need a room reload
- Bunny sequencer
- WIP
- Plays the flute automatically based on pixels on the mural
- Blues and reds are half notes, beiges full notes
- Take out the flute to play your song while in the room
- Asset mods
- WIP, untested, these things might change on a whim

### Mods

- WIP, untested, these things might still change on a whim
- Loads correctly named and formatted loose files recursively from `MAXWELL/Mods`
- No error checking
- All original maps, assets and textures can be dumped with the [level editor](https://github.com/Redcrafter/Animal-Well-editor)
- Supports 3 types of modded files (all files in a mod must be placed in one of these subdirectories)
- Map mods in `[Mod Name]/Maps/X.map` where `X` is a map index in `0..4`
- Full asset mods in `[Mod Name]/Assets/Y.whatever` where `Y` is an asset number as dumped by the editor
- Texture atlas mods in `[Mod Name]/Tiles/Z.png` where `Z` is a texture number as dumped by the editor, or maybe at least matching an accompanying asset 254 (the texture atlas descriptor)
- Behavior with multiple conflicting mods is undefined, but probably at least one of the conflicing files will be loaded
- Behavior with unexpected file formats is almost definitely crashing
- Examples:
- `MAXWELL/Mods/2.map` replaces space map
- `MAXWELL/Mods/Cool level/0.map` replaces well map
- `MAXWELL/Mods/Textures/255.png` replaces main texture atlas
- `MAXWELL/Mods/Farts/101.ogg` replaces dog barks with farts
- `MAXWELL/Mods/Epic mod/Maps/0.map` replaces well map
- `MAXWELL/Mods/Epic mod/Assets/101.ogg` replaces dog barks with farts
- `MAXWELL/Mods/Epic mod/Tiles/416.png` replaces only the beanie texture in main texture atlas
- `MAXWELL/Mods/Epic mod/Tiles/365.png` replaces another thing in main texture atlas
- `MAXWELL/Mods/Bad mod/Assets/255.png` replaces whole main texture atlas (don't do this, it's dumb and not compatible with multiple mods)
- These are the only asset file types currently tested, but others might also work
- **MAXWELL.exe has to be the one launching the game to hook the asset loading functions early**
- This can be done with the `--launch_game` command line switch or putting it in the right folder and just hitting enter when it asks
- Mods won't load correctly if you just launch the game from Steam and have MAXWELL searching for processes
157 changes: 119 additions & 38 deletions max.cpp
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
#include "max.h"

#include "shlwapi.h"
#include <Windows.h>
#include <algorithm>
#include <array>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <vector>

#include "detours.h"
#include "ghidra_byte_string.h"
Expand Down Expand Up @@ -120,6 +124,8 @@ AssetInfo HookGetAsset(uint32_t id) {
auto asset = g_get_asset_trampoline(id);
Max::get().load_custom_asset(id, asset);
// DEBUG("GetAsset: {} {}", id, asset.data);
if (id == 255)
Max::get().load_tile_mods(asset);
return asset;
}

Expand All @@ -136,7 +142,7 @@ using SetupGame = void(void *);
SetupGame *g_setup_game_trampoline{nullptr};
void HookSetupGame(void *a) {
g_setup_game_trampoline(a);
Max::get().load_mods();
Max::get().load_map_mods();
}

inline bool &get_is_init() {
Expand Down Expand Up @@ -324,28 +330,28 @@ Player Max::player() {
return (Player)(*(size_t *)get_address("slots") + 0x93670);
}

Coord *Max::player_room() { return (Coord *)(player() + 0x20); }
S32Vec2 *Max::player_room() { return (S32Vec2 *)(player() + 0x20); }

fCoord *Max::player_position() { return (fCoord *)(player()); }
FVec2 *Max::player_position() { return (FVec2 *)(player()); }

fCoord *Max::player_velocity() { return (fCoord *)(player() + 0x8); }
FVec2 *Max::player_velocity() { return (FVec2 *)(player() + 0x8); }

fCoord *Max::player_wheel() {
return (fCoord *)(*(size_t *)get_address("slots") + 0x9b0c0);
FVec2 *Max::player_wheel() {
return (FVec2 *)(*(size_t *)get_address("slots") + 0x9b0c0);
}

int *Max::player_map() {
return (int *)(*(size_t *)get_address("layer_base") +
*(uint32_t *)get_address("layer_offset"));
}

Coord *Max::respawn_room() { return (Coord *)(player() + 0x98); }
S32Vec2 *Max::respawn_room() { return (S32Vec2 *)(player() + 0x98); }

Coord *Max::respawn_position() { return (Coord *)(player() + 0xa0); }
S32Vec2 *Max::respawn_position() { return (S32Vec2 *)(player() + 0xa0); }

Coord *Max::warp_room() { return (Coord *)(player() + 0x34); }
S32Vec2 *Max::warp_room() { return (S32Vec2 *)(player() + 0x34); }

Coord *Max::warp_position() { return (Coord *)(player() + 0x3c); }
S32Vec2 *Max::warp_position() { return (S32Vec2 *)(player() + 0x3c); }

int *Max::warp_map() { return (int *)(player() + 0x44); }

Expand All @@ -359,7 +365,7 @@ Directions *Max::player_directions() {

int8_t *Max::player_hp() { return (int8_t *)(slot() + 0x400 + 0x1cc); }

Coord *Max::spawn_room() { return (Coord *)(slot() + 0x400 + 0x1ec); }
S32Vec2 *Max::spawn_room() { return (S32Vec2 *)(slot() + 0x400 + 0x1ec); }

uint16_t *Max::equipment() { return (uint16_t *)(slot() + 0x400 + 0x1f4); }

Expand Down Expand Up @@ -487,18 +493,46 @@ AssetInfo *Max::get_asset(uint32_t id) {
return (AssetInfo *)(get_address("assets") + id * sizeof(AssetInfo));
}

std::string get_custom_asset_path(uint32_t id) {
std::vector<fs::path> get_sorted_mod_files() {
std::vector<fs::path> mod_files;
if (!fs::exists("MAXWELL\\Mods") || !fs::is_directory("MAXWELL\\Mods"))
return "";
for (const auto &p : fs::recursive_directory_iterator("MAXWELL\\Mods")) {
return mod_files;
std::copy(fs::recursive_directory_iterator("MAXWELL\\Mods"),
fs::recursive_directory_iterator(), std::back_inserter(mod_files));
std::sort(mod_files.begin(), mod_files.end());
return mod_files;
}

bool has_tile_mods() {
for (const auto &p : get_sorted_mod_files()) {
if (!fs::is_directory(p)) {
auto stem = p.path().stem().string();
auto ext = p.path().extension().string();
auto fileId = std::atoi(stem.c_str());
if (ext == ".map")
continue;
if (id == fileId && (fileId != 0 || stem == "0"))
return p.path().string();
auto parent = p.parent_path().filename().string();
std::transform(parent.begin(), parent.end(), parent.begin(),
[](unsigned char c) { return std::tolower(c); });
auto ext = p.extension().string();
std::transform(ext.begin(), ext.end(), ext.begin(),
[](unsigned char c) { return std::tolower(c); });
if (parent == "tiles" && ext == ".png")
return true;
}
}
return false;
}

std::string get_custom_asset_path(uint32_t id) {
for (const auto &p : get_sorted_mod_files()) {
if (!fs::is_directory(p)) {
auto file = p.string();
auto parent = p.parent_path().filename().string();
std::transform(parent.begin(), parent.end(), parent.begin(),
[](unsigned char c) { return std::tolower(c); });
auto stem = p.stem().string();
auto ext = p.extension().string();
std::transform(ext.begin(), ext.end(), ext.begin(),
[](unsigned char c) { return std::tolower(c); });
auto fileId = StrToInt(stem.c_str());
if (id == fileId && parent == "assets" && (fileId != 0 || stem == "0"))
return file;
}
}
return "";
Expand All @@ -515,39 +549,82 @@ void Max::load_custom_asset(uint32_t id, AssetInfo &asset) {
asset.data = buf;
asset.size = s;
assets[id] = asset;
// DEBUG("Loaded custom asset: {} {}", id, asset.data);
DEBUG("Loaded custom asset {} from {}", id, asset.data);
}
if (assets.contains(id)) {
asset = assets[id];
// DEBUG("Found custom asset: {} {}", id, asset.data);
}
}

void Max::load_mods() {
if (!fs::exists("MAXWELL\\Mods") || !fs::is_directory("MAXWELL\\Mods"))
return;
for (const auto &p : fs::recursive_directory_iterator("MAXWELL\\Mods")) {
void Max::load_map_mods() {
for (const auto &p : get_sorted_mod_files()) {
if (!fs::is_directory(p)) {
auto file = p.path().string();
auto stem = p.path().stem().string();
auto ext = p.path().extension().string();
auto id = std::atoi(stem.c_str());
if (ext == ".map") {
auto file = p.string();
auto parent = p.parent_path().filename().string();
std::transform(parent.begin(), parent.end(), parent.begin(),
[](unsigned char c) { return std::tolower(c); });
auto stem = p.stem().string();
auto ext = p.extension().string();
std::transform(ext.begin(), ext.end(), ext.begin(),
[](unsigned char c) { return std::tolower(c); });
auto id = StrToInt(stem.c_str());
if (ext == ".map" && id >= 0 && id <= 4) {
import_map(file, id);
// DEBUG("Loaded map {} from {}", id, p.path().string());
} else if (id != 0 || stem == "0") {
DEBUG("Loaded map {} from {}", id, file);
}
}
}
}

void Max::load_tile_mods(AssetInfo &asset) {
if (!has_tile_mods())
return;

Image *atlas = new Image((char *)asset.data, asset.size);
DEBUG("Created atlas texture for tile mods");

for (const auto &p : get_sorted_mod_files()) {
if (!fs::is_directory(p)) {
auto file = p.string();
auto parent = p.parent_path().filename().string();
std::transform(parent.begin(), parent.end(), parent.begin(),
[](unsigned char c) { return std::tolower(c); });
auto stem = p.stem().string();
auto ext = p.extension().string();
std::transform(ext.begin(), ext.end(), ext.begin(),
[](unsigned char c) { return std::tolower(c); });
auto id = StrToInt(stem.c_str());
if ((id != 0 || stem == "0") && parent == "tiles" && ext == ".png") {
size_t s = fs::file_size(file);
char *buf = (char *)malloc(s);
std::ifstream f(file.c_str(), std::ios::in | std::ios::binary);
f.read(buf, s);
get_asset(id)->flags &= 0x3f;
get_asset(id)->data = buf;
get_asset(id)->size = s;
assets[id] = *get_asset(id);
// DEBUG("Loaded mod {} from {}", id, file);
f.close();
auto img = Image(buf, s);
auto size = img.size();
auto pos = (*tile_uvs())[id].pos;
for (int y1 = 0; y1 < size.y; y1++) {
for (int x1 = 0; x1 < size.x; x1++) {
(*atlas)(pos.x + x1, pos.y + y1) = img(x1, y1);
}
}
DEBUG("Loaded tile {} from {}", id, file);
}
}
}

{
int len;
auto image = atlas->get_png(&len);
asset.data = (char *)malloc(len);
memcpy(asset.data, image, len);
asset.flags &= 0x3f;
asset.size = len;
assets[255] = asset;
delete atlas;
DEBUG("Saved atlas texture to asset 255");
}
}

// TODO: I don't know wtf this does
Expand All @@ -564,3 +641,7 @@ void Max::draw_text(int x, int y, const wchar_t *text) {
static DrawTextFunc *draw = (DrawTextFunc *)get_address("draw_text");
draw(x, y, text);
}

std::array<uv_data, 1024> *Max::tile_uvs() {
return (std::array<uv_data, 1024> *)((size_t)get_asset(254)->data + 0xc);
}
Loading

0 comments on commit b06b4a7

Please sign in to comment.