Compare commits

...

9 Commits

Author SHA1 Message Date
Skyth d64c7e3cc4 Shhh, don't tell Melpontro. 2025-02-27 14:10:11 +03:00
SuperSonic16 716200e7df Updated flatpak build script (#467) 2025-02-26 20:22:29 +03:00
Skyth (Asilkan) 999fa2af61 Implement shader archive decompressor for the build system. (#466)
* Implement shader archive decompressor for the build system.

* Fix Linux compilation error.
2025-02-26 13:49:36 +03:00
Hyper 821f5eba4b Allow the hint volume for s20n_mykETF_c_navi_2 (#465)
This one isn't really a hint and has inconsistent behaviour with the same scenario but during daytime, where that hint seems to be requested by the entrance gate object itself.
2025-02-26 10:31:14 +00:00
Skyth 762dbe0419 Update version milestone to Release Candidate 3. 2025-02-25 23:59:03 +03:00
Skyth (Asilkan) 0128377ad9 Implement installer music. (#463) 2025-02-25 23:00:51 +03:00
ĐeäTh 98daa27c14 Added exceptions for specific hint messages being allowed/blocked by the Hints option (#462)
* Fix Windmill Isle Act 1 (Night) chip 'hint' not being shown

* code style adjustments

* Fix Apotos entrance gate first time Chip hint appearing with hints disabled

---------

Co-authored-by: Hyper <34012267+hyperbx@users.noreply.github.com>
2025-02-25 18:34:57 +03:00
ĐeäTh 0b16633ee1 fix options menu reset to default logic getting executed when setting is already default (#461) 2025-02-24 17:17:47 +01:00
Skyth (Asilkan) 388a86e866 Fix shader recompiler not depending on the patched executable. (#453) 2025-02-23 18:07:11 +03:00
19 changed files with 8395 additions and 47 deletions
+8 -5
View File
@@ -14,30 +14,33 @@ jobs:
runs-on: ubuntu-24.04
steps:
- name: Checkout repository
- name: Checkout Repository
uses: actions/checkout@v4
with:
submodules: recursive
token: ${{ secrets.ORG_TOKEN }}
- name: Checkout private repository
- name: Checkout Private Repository
uses: actions/checkout@v4
with:
repository: ${{ secrets.ASSET_REPO }}
token: ${{ secrets.ASSET_REPO_TOKEN }}
path: flatpak/private
path: ./private
- name: Install dependencies
- name: Install Dependencies
run: |-
sudo apt update
sudo apt install -y flatpak-builder ccache
- name: Cache ccache directory
- name: Setup ccache
uses: actions/cache@v4
with:
path: /tmp/ccache
key: ccache-${{ runner.os }}
- name: Prepare Project
run: cp ./private/* ./UnleashedRecompLib/private
- name: Prepare Flatpak
run: |
flatpak --user remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo
+4
View File
@@ -553,6 +553,7 @@ BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/ga
BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/game_icon_night.bmp" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/game_icon_night.bmp" ARRAY_NAME "g_game_icon_night")
## Audio ##
BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/music/installer.ogg" DEST_FILE "${RESOURCES_OUTPUT_PATH}/music/installer.ogg" ARRAY_NAME "g_installer_music")
BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/sounds/sys_worldmap_cursor.ogg" DEST_FILE "${RESOURCES_OUTPUT_PATH}/sounds/sys_worldmap_cursor.ogg" ARRAY_NAME "g_sys_worldmap_cursor")
BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/sounds/sys_worldmap_finaldecide.ogg" DEST_FILE "${RESOURCES_OUTPUT_PATH}/sounds/sys_worldmap_finaldecide.ogg" ARRAY_NAME "g_sys_worldmap_finaldecide")
BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/sounds/sys_actstg_pausecansel.ogg" DEST_FILE "${RESOURCES_OUTPUT_PATH}/sounds/sys_actstg_pausecansel.ogg" ARRAY_NAME "g_sys_actstg_pausecansel")
@@ -560,3 +561,6 @@ BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/sounds/sy
BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/sounds/sys_actstg_pausedecide.ogg" DEST_FILE "${RESOURCES_OUTPUT_PATH}/sounds/sys_actstg_pausedecide.ogg" ARRAY_NAME "g_sys_actstg_pausedecide")
BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/sounds/sys_actstg_pausewinclose.ogg" DEST_FILE "${RESOURCES_OUTPUT_PATH}/sounds/sys_actstg_pausewinclose.ogg" ARRAY_NAME "g_sys_actstg_pausewinclose")
BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/sounds/sys_actstg_pausewinopen.ogg" DEST_FILE "${RESOURCES_OUTPUT_PATH}/sounds/sys_actstg_pausewinopen.ogg" ARRAY_NAME "g_sys_actstg_pausewinopen")
## Keyboard QTE ##
BIN2C(TARGET_OBJ UnleashedRecomp SOURCE_FILE "${RESOURCES_SOURCE_PATH}/images/common/raw/kb_key_0.png" DEST_FILE "${RESOURCES_OUTPUT_PATH}/images/common/raw/kb_key_0.png" ARRAY_NAME "g_kb_key_0" COMPRESSION_TYPE "zstd")
+1
View File
@@ -87,6 +87,7 @@
#include "SWA/Inspire/InspireTextureOverlay.h"
#include "SWA/Inspire/InspireTextureOverlayInfo.h"
#include "SWA/Menu/MenuWindowBase.h"
#include "SWA/Message/MsgRequestHelp.h"
#include "SWA/Movie/MovieDisplayer.h"
#include "SWA/Movie/MovieManager.h"
#include "SWA/Object/Common/DashPanel/ObjDashPanel.h"
@@ -0,0 +1,13 @@
#pragma once
#include <SWA.inl>
namespace SWA::Message
{
class MsgRequestHelp
{
public:
SWA_INSERT_PADDING(0x1C);
Hedgehog::Base::CSharedString m_Name;
};
}
+24 -1
View File
@@ -2,6 +2,7 @@
#include <apu/embedded_player.h>
#include <user/config.h>
#include <res/music/installer.ogg.h>
#include <res/sounds/sys_worldmap_cursor.ogg.h>
#include <res/sounds/sys_worldmap_finaldecide.ogg.h>
#include <res/sounds/sys_actstg_pausecansel.ogg.h>
@@ -87,13 +88,17 @@ static void PlayEmbeddedSound(EmbeddedSound s)
data.chunk = Mix_LoadWAV_RW(SDL_RWFromConstMem(soundData, soundDataSize), 1);
}
Mix_VolumeChunk(data.chunk, Config::MasterVolume * Config::EffectsVolume * MIX_MAX_VOLUME);
Mix_PlayChannel(g_channelIndex % MIX_CHANNELS, data.chunk, 0);
++g_channelIndex;
}
static Mix_Music* g_installerMusic;
void EmbeddedPlayer::Init()
{
Mix_OpenAudio(XAUDIO_SAMPLES_HZ, AUDIO_F32SYS, 2, 256);
Mix_OpenAudio(XAUDIO_SAMPLES_HZ, AUDIO_F32SYS, 2, 2048);
g_installerMusic = Mix_LoadMUS_RW(SDL_RWFromConstMem(g_installer_music, sizeof(g_installer_music)), 1);
s_isActive = true;
}
@@ -111,6 +116,21 @@ void EmbeddedPlayer::Play(const char *name)
PlayEmbeddedSound(it->second);
}
void EmbeddedPlayer::PlayMusic()
{
if (!Mix_PlayingMusic())
{
Mix_PlayMusic(g_installerMusic, INT_MAX);
Mix_VolumeMusic(Config::MasterVolume * Config::MusicVolume * MUSIC_VOLUME * MIX_MAX_VOLUME);
}
}
void EmbeddedPlayer::FadeOutMusic()
{
if (Mix_PlayingMusic())
Mix_FadeOutMusic(1000);
}
void EmbeddedPlayer::Shutdown()
{
for (EmbeddedSoundData &data : g_embeddedSoundData)
@@ -119,6 +139,9 @@ void EmbeddedPlayer::Shutdown()
Mix_FreeChunk(data.chunk);
}
Mix_HaltMusic();
Mix_FreeMusic(g_installerMusic);
Mix_CloseAudio();
Mix_Quit();
+6 -1
View File
@@ -2,9 +2,14 @@
struct EmbeddedPlayer
{
inline static bool s_isActive = false;
// Arbitrarily picked volume to match the mixing in the original game.
static constexpr float MUSIC_VOLUME = 0.25f;
static inline bool s_isActive = false;
static void Init();
static void Play(const char *name);
static void PlayMusic();
static void FadeOutMusic();
static void Shutdown();
};
+5
View File
@@ -1350,6 +1350,9 @@ struct ImGuiPushConstants
extern ImFontBuilderIO g_fontBuilderIO;
extern void InitKeyboardQTE();
extern void DrawKeyboardQTE();
static void CreateImGuiBackend()
{
ImGuiIO& io = ImGui::GetIO();
@@ -1372,6 +1375,7 @@ static void CreateImGuiBackend()
MessageWindow::Init();
OptionsMenu::Init();
InstallerWizard::Init();
InitKeyboardQTE();
ImGui_ImplSDL2_InitForOther(GameWindow::s_pWindow);
@@ -2464,6 +2468,7 @@ static void DrawImGui()
InstallerWizard::Draw();
MessageWindow::Draw();
ButtonGuide::Draw();
DrawKeyboardQTE();
Fader::Draw();
BlackBar::Draw();
+228
View File
@@ -108,3 +108,231 @@ PPC_FUNC(sub_82586698)
__imp__sub_82586698(ctx, base);
}
// SWA::CObjHint::MsgNotifyObjectEvent::Impl
// Disable only certain hints from hint volumes.
// This hook should be used to allow hint volumes specifically to also prevent them from affecting the player.
PPC_FUNC_IMPL(__imp__sub_82736E80);
PPC_FUNC(sub_82736E80)
{
// GroupID parameter text
auto* groupId = (const char*)(base + PPC_LOAD_U32(ctx.r3.u32 + 0x100));
if (!Config::Hints)
{
// WhiteIsland_ACT1_001: "Your friend went off that way, Sonic. Quick, let's go after him!"
// s20n_mykETF_c_navi_2: "Huh? Weird! We can't get through here anymore. We were able to earlier!"
if (strcmp(groupId, "WhiteIsland_ACT1_001") != 0 && strcmp(groupId, "s20n_mykETF_c_navi_2") != 0)
return;
}
__imp__sub_82736E80(ctx, base);
}
// SWA::CHelpWindow::MsgRequestHelp::Impl
// Disable only certain hints from other sequences.
// This hook should be used to block hint messages from unknown sources.
PPC_FUNC_IMPL(__imp__sub_824C1E60);
PPC_FUNC(sub_824C1E60)
{
auto pMsgRequestHelp = (SWA::Message::MsgRequestHelp*)(base + ctx.r4.u32);
if (!Config::Hints)
{
// s10d_mykETF_c_navi: "Looks like we can get to a bunch of places in the village from here!"
if (strcmp(pMsgRequestHelp->m_Name.c_str(), "s10d_mykETF_c_navi") == 0)
return;
}
__imp__sub_824C1E60(ctx, base);
}
#include <gpu/video.h>
#include <gpu/imgui/imgui_snapshot.h>
#include <res/images/common/raw/kb_key_0.png.h>
#include <decompressor.h>
#include <ui/imgui_utils.h>
#include <exports.h>
#include <words.h>
#include <random>
static std::string g_qteText;
static std::vector<double> g_qteTimes;
static bool g_shouldDrawKeyboardQTE;
static std::unique_ptr<GuestTexture> g_keyboardTexture;
static uint8_t g_prevScanCodes[SDL_NUM_SCANCODES];
static std::default_random_engine g_engine{ std::random_device {}() };
static std::uniform_int_distribution g_intDistribution;
static std::uniform_real_distribution g_floatDistribution(0.0, 1.0);
void InitKeyboardQTE()
{
g_keyboardTexture = LOAD_ZSTD_TEXTURE(g_kb_key_0);
}
void DrawKeyboardQTE()
{
memcpy(g_prevScanCodes, SDL_GetKeyboardState(nullptr), sizeof(g_prevScanCodes));
if (!g_shouldDrawKeyboardQTE)
return;
g_shouldDrawKeyboardQTE = false;
auto drawList = ImGui::GetBackgroundDrawList();
ImFont* font = ImFontAtlasSnapshot::GetFont("FOT-NewRodinPro-DB.otf");
// y: 307
// h: 110
// font: x 36 y 30 30pt
auto& res = ImGui::GetIO().DisplaySize;
constexpr float padding = -40.0f;
float width = Scale(g_qteText.size() * 110 + (g_qteText.size() - 1) * padding);
for (size_t i = 0; i < g_qteText.size(); i++)
{
float x = res.x / 2.0f - width / 2.0f + Scale((110 + padding)) * i;
double motion = ComputeLinearMotion(g_qteTimes[i], 0, 5);
if (g_qteText[i] != ' ')
{
std::stringstream text;
text << std::toupper(g_qteText[i], std::locale::classic());
float x = res.x / 2.0f - width / 2.0f + Scale((110 + padding)) * i;
drawList->AddImage(g_keyboardTexture.get(), { x, Scale(307) }, { x + Scale(110), Scale(307 + 110) }, {0.0f, 0.0f}, {1.0f, 1.0f}, IM_COL32(255, 255, 255, 255 * motion));
drawList->AddText(font, Scale(24.0f), { x + Scale(36), Scale(307 + 28) }, IM_COL32(0, 0, 0, 255 * motion), text.str().c_str());
}
else
{
drawList->AddImage(g_keyboardTexture.get(), { x, Scale(307) }, { x + Scale(110), Scale(307 + 110) }, { 0.0f, 0.0f }, { 1.0f, 1.0f }, IM_COL32(255, 255, 255, 255 * (1.0 - motion)));
}
}
}
// + 100 is success bool
// return true to indicate either succeeded or failed
PPC_FUNC_IMPL(__imp__sub_823329F8);
PPC_FUNC(sub_823329F8)
{
struct DelayCall
{
PPCRegister r3;
uint8_t* base;
PPCRegister time;
DelayCall(PPCRegister& r3, uint8_t* base) : r3(r3), base(base)
{
time.u32 = PPC_LOAD_U32(r3.u32 + 104);
PPCRegister newTime = time;
newTime.f32 *= 5.0f;
PPC_STORE_U32(r3.u32 + 104, newTime.u32);
}
~DelayCall()
{
PPC_STORE_U32(r3.u32 + 104, time.u32);
}
} delayCall(ctx.r3, base);
g_shouldDrawKeyboardQTE = true;
bool foundAny = false;
for (size_t i = 0; i < g_qteText.size(); i++)
{
if (g_qteText[i] != ' ')
{
int lower = std::tolower(g_qteText[i], std::locale::classic());
SDL_Scancode scancode = SDL_GetScancodeFromKey(lower);
for (size_t j = SDL_SCANCODE_A; j <= SDL_SCANCODE_Z; j++)
{
if (g_prevScanCodes[j] == 0 && SDL_GetKeyboardState(nullptr)[j] != 0)
{
if (j == scancode) // pressed the right one
{
g_qteText[i] = ' ';
g_qteTimes[i] = ImGui::GetTime();
ctx.r3.u32 = 0;
Game_PlaySound("objsn_trickjump_button");
return;
}
else // wrong one!
{
PPC_STORE_U8(ctx.r3.u32 + 100, 0);
ctx.r3.u32 = 1;
return;
}
}
}
foundAny = true;
break;
}
}
// ran out of time
if (PPC_LOAD_U32(ctx.r3.u32 + 132) >= PPC_LOAD_U32(ctx.r3.u32 + 104))
{
PPC_STORE_U8(ctx.r3.u32 + 100, 0);
ctx.r3.u32 = 1;
return;
}
if (!foundAny)
{
// pressed all of them correctly
PPC_STORE_U8(ctx.r3.u32 + 100, 1);
ctx.r3.u32 = 1;
return;
}
__imp__sub_823329F8(ctx, base);
ctx.r3.u32 = 0;
return;
}
PPC_FUNC_IMPL(__imp__sub_826117E0);
PPC_FUNC(sub_826117E0)
{
auto counts = reinterpret_cast<uint32_t*>(base + PPC_LOAD_U32(ctx.r3.u32 + 252));
auto times = reinterpret_cast<uint32_t*>(base + PPC_LOAD_U32(ctx.r3.u32 + 236));
for (size_t i = 1; i < 3; i++)
{
if (counts[i] == 0)
{
counts[i] = counts[0];
times[i] = times[0];
}
}
__imp__sub_826117E0(ctx, base);
}
void QteButtonPromptInitMidAsmHook()
{
g_qteText = g_words[g_intDistribution(g_engine) % std::size(g_words)];
g_qteTimes.clear();
g_qteTimes.resize(g_qteText.size(), ImGui::GetTime());
}
void QteButtonPromptUpdateMidAsmHook()
{
}
void TrickJumperCompareMidAsmHook(PPCRegister& r28)
{
if (r28.u32 == 12)
{
if (g_floatDistribution(g_engine) < 0.3)
r28.u32 = 0;
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
VERSION_MILESTONE="Release Candidate 2"
VERSION_MILESTONE="Release Candidate 3"
VERSION_MAJOR=1
VERSION_MINOR=0
VERSION_REVISION=0
+19
View File
@@ -1721,6 +1721,24 @@ static void PickerCheckResults()
g_currentPickerVisible = false;
}
static bool g_fadingOutMusic;
static void ProcessMusic()
{
if (g_isDisappearing)
{
if (!g_fadingOutMusic)
{
EmbeddedPlayer::FadeOutMusic();
g_fadingOutMusic = true;
}
}
else
{
EmbeddedPlayer::PlayMusic();
}
}
void InstallerWizard::Init()
{
auto &io = ImGui::GetIO();
@@ -1850,6 +1868,7 @@ bool InstallerWizard::Run(std::filesystem::path installPath, bool skipGame)
while (s_isVisible)
{
Video::WaitOnSwapChain();
ProcessMusic();
SDL_PumpEvents();
SDL_FlushEvents(SDL_FIRSTEVENT, SDL_LASTEVENT);
GameWindow::Update();
+10 -7
View File
@@ -859,16 +859,19 @@ static void DrawConfigOption(int32_t rowIndex, float yOffset, ConfigDef<T>* conf
if (g_canReset && padState.IsTapped(SWA::eKeyState_X))
{
config->MakeDefault();
if (!config->IsDefaultValue())
{
config->MakeDefault();
VideoConfigValueChangedCallback(config);
XAudioConfigValueChangedCallback(config);
VideoConfigValueChangedCallback(config);
XAudioConfigValueChangedCallback(config);
if (config->Callback)
config->Callback(config);
if (config->Callback)
config->Callback(config);
if (config->ApplyCallback)
config->ApplyCallback(config);
if (config->ApplyCallback)
config->ApplyCallback(config);
}
Game_PlaySound("sys_worldmap_decide");
}
File diff suppressed because it is too large Load Diff
+23 -5
View File
@@ -26,14 +26,26 @@ foreach(i RANGE 0 260)
endforeach()
add_custom_command(
OUTPUT ${UNLEASHED_RECOMP_PPC_RECOMPILED_SOURCES}
COMMAND $<TARGET_FILE:XenonRecomp>
OUTPUT
"${CMAKE_CURRENT_SOURCE_DIR}/private/default_patched.xex"
${UNLEASHED_RECOMP_PPC_RECOMPILED_SOURCES}
COMMAND
$<TARGET_FILE:XenonRecomp>
DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/private/default.xex"
"${CMAKE_CURRENT_SOURCE_DIR}/private/default.xexp"
"${CMAKE_CURRENT_SOURCE_DIR}/config/SWA.toml"
)
add_custom_command(
OUTPUT
"${CMAKE_CURRENT_SOURCE_DIR}/private/shader_decompressed.ar"
COMMAND
$<TARGET_FILE:x_decompress> "${CMAKE_CURRENT_SOURCE_DIR}/private/shader.ar" "${CMAKE_CURRENT_SOURCE_DIR}/private/shader_decompressed.ar"
DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/private/shader.ar"
)
set(XENOS_RECOMP_ROOT "${UNLEASHED_RECOMP_TOOLS_ROOT}/XenosRecomp/XenosRecomp")
set(XENOS_RECOMP_INCLUDE "${XENOS_RECOMP_ROOT}/shader_common.h")
@@ -49,9 +61,15 @@ file(GLOB XENOS_RECOMP_SOURCES
)
add_custom_command(
OUTPUT "${CMAKE_CURRENT_SOURCE_DIR}/shader/shader_cache.cpp"
COMMAND $<TARGET_FILE:XenosRecomp>
DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/private/shader.ar" ${XENOS_RECOMP_SOURCES} ${XENOS_RECOMP_INCLUDE}
OUTPUT
"${CMAKE_CURRENT_SOURCE_DIR}/shader/shader_cache.cpp"
COMMAND
$<TARGET_FILE:XenosRecomp>
DEPENDS
"${CMAKE_CURRENT_SOURCE_DIR}/private/default_patched.xex"
"${CMAKE_CURRENT_SOURCE_DIR}/private/shader_decompressed.ar"
${XENOS_RECOMP_SOURCES}
${XENOS_RECOMP_INCLUDE}
)
add_library(UnleashedRecompLib
+21 -6
View File
@@ -106,18 +106,18 @@ jump_address = 0x82468EE0
name = "ResetScoreOnRestartMidAsmHook"
address = 0x82304374
# Disable hint volumes
[[midasm_hook]]
name = "DisableHintsMidAsmHook"
address = 0x827A2504
jump_address_on_true = 0x827A251C
# Disable hint rings
[[midasm_hook]]
name = "DisableHintsMidAsmHook"
address = 0x827A2E34
jump_address_on_true = 0x827A2E4C
# Disable Tornado Defense hints
[[midasm_hook]]
name = "DisableHintsMidAsmHook"
address = 0x82AF52BC
jump_address_on_true = 0x82AF52E4
# Disable Egg Dragoon hint "V_WHG_083" ("That lit-up part on the bottom looks fishy. I'll try aiming for that.")
[[midasm_hook]]
name = "DisableHintsMidAsmHook"
@@ -1093,3 +1093,18 @@ registers = ["r31", "r29", "r28"]
name = "ObjGrindDashPanelAllocMidAsmHook"
address = 0x82614948
registers = ["r3"]
[[midasm_hook]]
name = "QteButtonPromptInitMidAsmHook"
address = 0x823339D8
jump_address = 0x82333D14
[[midasm_hook]]
name = "QteButtonPromptUpdateMidAsmHook"
address = 0x82332A90
jump_address = 0x82332BC4
[[midasm_hook]]
name = "TrickJumperCompareMidAsmHook"
address = 0x82611910
registers = ["r28"]
@@ -32,26 +32,6 @@
{
"type": "dir",
"path": "../"
},
{
"type": "file",
"path": "private/default.xex",
"dest": "UnleashedRecompLib/private"
},
{
"type": "file",
"path": "private/default.xexp",
"dest": "UnleashedRecompLib/private"
},
{
"type": "file",
"path": "private/default_patched.xex",
"dest": "UnleashedRecompLib/private"
},
{
"type": "file",
"path": "private/shader.ar",
"dest": "UnleashedRecompLib/private"
}
],
"build-options": {
+1
View File
@@ -1,5 +1,6 @@
add_subdirectory(${UNLEASHED_RECOMP_TOOLS_ROOT}/bc_diff)
add_subdirectory(${UNLEASHED_RECOMP_TOOLS_ROOT}/file_to_c)
add_subdirectory(${UNLEASHED_RECOMP_TOOLS_ROOT}/fshasher)
add_subdirectory(${UNLEASHED_RECOMP_TOOLS_ROOT}/x_decompress)
add_subdirectory(${UNLEASHED_RECOMP_TOOLS_ROOT}/XenonRecomp)
add_subdirectory(${UNLEASHED_RECOMP_TOOLS_ROOT}/XenosRecomp)
+11
View File
@@ -0,0 +1,11 @@
project("x_decompress")
set(CMAKE_CXX_STANDARD 17)
add_executable(x_decompress
"x_decompress.cpp"
"${UNLEASHED_RECOMP_TOOLS_ROOT}/XenonRecomp/thirdparty/libmspack/libmspack/mspack/lzxd.c"
)
target_include_directories(x_decompress
PRIVATE "${UNLEASHED_RECOMP_TOOLS_ROOT}/XenonRecomp/thirdparty/libmspack/libmspack/mspack"
)
+247
View File
@@ -0,0 +1,247 @@
#include <algorithm>
#include <cstdint>
#include <cstddef>
#include <cstring>
#include <vector>
#include <fstream>
#include <cassert>
#include <mspack.h>
#include <lzx.h>
static std::vector<uint8_t> readAllBytes(const char* path)
{
std::ifstream file{ path, std::ios::binary };
std::vector<uint8_t> result{};
if (!file.good()) {
return result;
}
file.seekg(0, std::ios::end);
result.resize(file.tellg());
file.seekg(0, std::ios::beg);
file.read(reinterpret_cast<char*>(result.data()), result.size());
return result;
}
template<typename T>
static T byteSwap(T value)
{
if constexpr (sizeof(T) == 1)
return value;
else if constexpr (sizeof(T) == 2)
return static_cast<T>(__builtin_bswap16(static_cast<uint16_t>(value)));
else if constexpr (sizeof(T) == 4)
return static_cast<T>(__builtin_bswap32(static_cast<uint32_t>(value)));
else if constexpr (sizeof(T) == 8)
return static_cast<T>(__builtin_bswap64(static_cast<uint64_t>(value)));
assert(false && "Unexpected byte size.");
return value;
}
template<typename T>
static void byteSwapInplace(T& value)
{
value = byteSwap(value);
}
struct ReadStream
{
const uint8_t* data = nullptr;
int size = 0; // Size from every compressed block.
};
static int mspackRead(mspack_file* file, void* buffer, int bytes)
{
ReadStream* stream = reinterpret_cast<ReadStream*>(file);
if (stream->size == 0)
{
uint16_t size = byteSwap(*reinterpret_cast<const uint16_t*>(stream->data));
stream->data += sizeof(uint16_t);
// This indicates there is an uncompressed block size available. We don't need it so we skip it.
if ((size & 0xFF00) == 0xFF00)
{
stream->data += 1;
size = byteSwap(*reinterpret_cast<const uint16_t*>(stream->data));
stream->data += sizeof(uint16_t);
}
stream->size = size;
}
int sizeToRead = std::min(stream->size, bytes);
memcpy(buffer, stream->data, sizeToRead);
stream->data += sizeToRead;
stream->size -= sizeToRead;
return sizeToRead;
}
struct WriteStream
{
uint8_t* data = nullptr;
std::size_t size = 0; // Remaining available space in the stream.
};
static int mspackWrite(mspack_file* file, void* buffer, int bytes)
{
WriteStream* stream = reinterpret_cast<WriteStream*>(file);
std::size_t sizeToWrite = std::min(stream->size, static_cast<std::size_t>(bytes));
memcpy(stream->data, buffer, sizeToWrite);
stream->data += sizeToWrite;
stream->size -= sizeToWrite;
return static_cast<int>(sizeToWrite);
}
static void* mspackAlloc(mspack_system* self, size_t bytes)
{
return operator new(bytes);
}
static void mspackFree(void* ptr)
{
operator delete(ptr);
}
static void mspackCopy(void* src, void* dst, size_t bytes)
{
memcpy(dst, src, bytes);
}
static mspack_system g_lzxSystem =
{
nullptr,
nullptr,
mspackRead,
mspackWrite,
nullptr,
nullptr,
nullptr,
mspackAlloc,
mspackFree,
mspackCopy
};
// Xbox Compression header definitions.
static constexpr uint32_t XCompressSignature = 0xFF512EE;
struct XCompressHeader
{
uint32_t signature;
uint32_t field04;
uint32_t field08;
uint32_t field0C;
uint32_t windowSize;
uint32_t compressedBlockSize;
uint64_t uncompressedSize;
uint64_t compressedSize;
uint32_t uncompressedBlockSize;
uint32_t field2C;
void byteSwap()
{
byteSwapInplace(signature);
byteSwapInplace(field04);
byteSwapInplace(field08);
byteSwapInplace(field0C);
byteSwapInplace(windowSize);
byteSwapInplace(compressedBlockSize);
byteSwapInplace(uncompressedSize);
byteSwapInplace(compressedSize);
byteSwapInplace(uncompressedBlockSize);
byteSwapInplace(field2C);
}
};
int main(int argc, char** argv)
{
if (argc < 3)
{
printf("Usage: x_decompress [input file path] [output file path]");
return EXIT_SUCCESS;
}
std::vector<uint8_t> file = readAllBytes(argv[1]);
if (file.empty())
{
fprintf(stderr, "Input file \"%s\" not found or empty", argv[1]);
return EXIT_FAILURE;
}
std::vector<uint8_t> decompressedFile;
if (file.size() >= sizeof(XCompressHeader) && byteSwap(*reinterpret_cast<uint32_t*>(file.data())) == XCompressSignature)
{
XCompressHeader* header = reinterpret_cast<XCompressHeader*>(file.data());
header->byteSwap();
decompressedFile.resize(header->uncompressedSize);
const uint8_t* srcBytes = file.data() + sizeof(XCompressHeader);
WriteStream dstStream;
dstStream.data = decompressedFile.data();
dstStream.size = decompressedFile.size();
// libmspack wants the bit index. This value is always guaranteed to be a power of two,
// so we can extract the bit index by counting the amount of leading zeroes.
int windowBits = 0;
uint32_t windowSize = header->windowSize;
while ((windowSize & 0x1) == 0)
{
++windowBits;
windowSize >>= 1;
}
// Loop over compressed blocks.
while (srcBytes < (file.data() + file.size()) && dstStream.data < (decompressedFile.data() + decompressedFile.size()))
{
uint32_t compressedSize = byteSwap(*reinterpret_cast<const uint32_t*>(srcBytes));
srcBytes += sizeof(uint32_t);
ReadStream srcStream;
srcStream.data = srcBytes;
std::size_t uncompressedBlockSize = std::min(static_cast<std::size_t>(header->uncompressedBlockSize), dstStream.size);
lzxd_stream* lzx = lzxd_init(
&g_lzxSystem,
reinterpret_cast<mspack_file*>(&srcStream),
reinterpret_cast<mspack_file*>(&dstStream),
windowBits,
0,
static_cast<int>(header->compressedBlockSize),
static_cast<off_t>(uncompressedBlockSize),
0);
lzxd_decompress(lzx, uncompressedBlockSize);
lzxd_free(lzx);
srcBytes += compressedSize;
}
}
else
{
decompressedFile = std::move(file);
}
std::ofstream outputFile(argv[2], std::ios::binary);
if (!outputFile.good())
{
fprintf(stderr, "Cannot open output file \"%s\" for writing", argv[2]);
return EXIT_FAILURE;
}
outputFile.write(reinterpret_cast<const char*>(decompressedFile.data()), decompressedFile.size());
return EXIT_SUCCESS;
}