Add LceLive UI and Windows64 integration

- Implement UIScene_LceLive class for managing the LceLive UI components.
- Create Windows64_LceLive.cpp and Windows64_LceLive.h for handling LceLive authentication and device linking.
- Introduce a Python script for repacking MediaWindows64.arc archives with overlay support.
- Enhance error handling and session management for LceLive interactions.
- Add methods for reading and writing UTF-8 strings in the archive format.
- Implement functionality to apply overlays to existing archive entries.
This commit is contained in:
veroxsity
2026-04-16 20:21:06 +01:00
parent 523c96198e
commit 3d6144e10f
23 changed files with 1452 additions and 4 deletions

View File

@@ -51,9 +51,11 @@ set_target_properties(Minecraft.Client PROPERTIES
target_link_libraries(Minecraft.Client PRIVATE
Minecraft.World
crypt32
d3d11
d3dcompiler
XInput9_1_0
winhttp
wsock32
legacy_stdio_definitions
$<$<CONFIG:Debug>: # Debug 4J libraries

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -65,6 +65,7 @@
#include "UIScene_TrialExitUpsell.h"
#include "UIScene_Intro.h"
#include "UIScene_LceLive.h"
#include "UIScene_SaveMessage.h"
#include "UIScene_MainMenu.h"
#include "UIScene_LoadMenu.h"
@@ -125,4 +126,4 @@
#include "UIScene_EULA.h"
#include "UIScene_NewUpdateMessage.h"
extern ConsoleUIController ui;
extern ConsoleUIController ui;

View File

@@ -121,6 +121,7 @@ enum EUIScene
eUIComponent_Tooltips,
eUIComponent_PressStartToPlay,
eUIComponent_MenuBackground,
eUIScene_LceLive,
eUIScene_Keyboard,
eUIScene_QuadrantSignin,
eUIScene_MessageBox,

View File

@@ -364,6 +364,9 @@ bool UILayer::NavigateToScene(int iPad, EUIScene scene, void *initData)
case eUIScene_SaveMessage:
newScene = new UIScene_SaveMessage(iPad, initData, this);
break;
case eUIScene_LceLive:
newScene = new UIScene_LceLive(iPad, initData, this);
break;
case eUIScene_MainMenu:
newScene = new UIScene_MainMenu(iPad, initData, this);
break;
@@ -906,4 +909,4 @@ UIScene *UILayer::FindScene(EUIScene sceneType)
}
return nullptr;
}
}

View File

@@ -0,0 +1,198 @@
#include "stdafx.h"
#include "UI.h"
#include "UIScene_LceLive.h"
#include "../../../Minecraft.World/StringHelpers.h"
#if defined(_WINDOWS64) && !defined(MINECRAFT_SERVER_BUILD)
#include "../../Windows64/Windows64_LceLive.h"
#endif
UIScene_LceLive::UIScene_LceLive(int iPad, void *initData, UILayer *parentLayer) : UIScene(iPad, parentLayer)
{
initialiseMovie();
parentLayer->addComponent(iPad, eUIComponent_Panorama);
parentLayer->addComponent(iPad, eUIComponent_Logo);
m_buttonEnabled = true;
m_descriptionApplied = false;
m_buttonPrimaryAction.init(L"START LINK", eControl_PrimaryAction);
m_labelTitle.init(L"LCELIVE");
m_labelDescription.init(L"");
IggyDataValue result;
IggyDataValue value[2];
value[0].type = IGGY_DATATYPE_number;
value[0].number = 1;
value[1].type = IGGY_DATATYPE_number;
value[1].number = 0;
IggyPlayerCallMethodRS(getMovie(), &result, IggyPlayerRootPath(getMovie()), m_funcInit, 2, value);
IggyPlayerCallMethodRS(getMovie(), &result, IggyPlayerRootPath(getMovie()), m_funcAutoResize, 0, nullptr);
}
UIScene_LceLive::~UIScene_LceLive()
{
m_parentLayer->removeComponent(eUIComponent_Panorama);
m_parentLayer->removeComponent(eUIComponent_Logo);
}
wstring UIScene_LceLive::getMoviePath()
{
return L"LceLive";
}
void UIScene_LceLive::updateTooltips()
{
if (m_buttonEnabled)
ui.SetTooltips(DEFAULT_XUI_MENU_USER, IDS_TOOLTIPS_SELECT, IDS_TOOLTIPS_BACK);
else
ui.SetTooltips(DEFAULT_XUI_MENU_USER, -1, IDS_TOOLTIPS_BACK);
}
void UIScene_LceLive::updateComponents()
{
m_parentLayer->showComponent(m_iPad, eUIComponent_Panorama, true);
m_parentLayer->showComponent(m_iPad, eUIComponent_Logo, true);
}
void UIScene_LceLive::tick()
{
UIScene::tick();
RefreshUi(false);
}
void UIScene_LceLive::handleReload()
{
m_descriptionApplied = false;
IggyDataValue result;
IggyDataValue value[2];
value[0].type = IGGY_DATATYPE_number;
value[0].number = 1;
value[1].type = IGGY_DATATYPE_number;
value[1].number = 0;
IggyPlayerCallMethodRS(getMovie(), &result, IggyPlayerRootPath(getMovie()), m_funcInit, 2, value);
IggyPlayerCallMethodRS(getMovie(), &result, IggyPlayerRootPath(getMovie()), m_funcAutoResize, 0, nullptr);
RefreshUi(true);
}
void UIScene_LceLive::handleInput(int iPad, int key, bool repeat, bool pressed, bool released, bool &handled)
{
ui.AnimateKeyPress(m_iPad, key, repeat, pressed, released);
switch (key)
{
case ACTION_MENU_CANCEL:
if (pressed && !repeat)
navigateBack();
break;
case ACTION_MENU_OK:
if (pressed && !repeat && m_buttonEnabled)
{
handled = true;
handlePress(static_cast<F64>(eControl_PrimaryAction), 0.0);
}
break;
#ifdef __ORBIS__
case ACTION_MENU_TOUCHPAD_PRESS:
if (pressed && !repeat && m_buttonEnabled)
{
handled = true;
handlePress(static_cast<F64>(eControl_PrimaryAction), 0.0);
}
break;
#endif
case ACTION_MENU_DOWN:
case ACTION_MENU_UP:
case ACTION_MENU_PAGEUP:
case ACTION_MENU_PAGEDOWN:
case ACTION_MENU_OTHER_STICK_DOWN:
case ACTION_MENU_OTHER_STICK_UP:
sendInputToMovie(key, repeat, pressed, released);
break;
}
}
void UIScene_LceLive::handlePress(F64 controlId, F64 childId)
{
if (static_cast<int>(controlId) != eControl_PrimaryAction || !m_buttonEnabled)
return;
#if defined(_WINDOWS64) && !defined(MINECRAFT_SERVER_BUILD)
const Win64LceLive::Snapshot snapshot = Win64LceLive::GetSnapshot();
ui.PlayUISFX(eSFX_Press);
if (snapshot.state == Win64LceLive::EClientState::SignedIn)
Win64LceLive::SignOut();
else
Win64LceLive::StartDeviceLink();
#endif
}
void UIScene_LceLive::RefreshUi(bool force)
{
std::wstring buttonLabel = L"LCELIVE UNAVAILABLE";
std::wstring description = L"LCELIVE\r\n\r\nThis build does not provide the Windows64 LCELive client runtime.";
bool buttonEnabled = false;
#if defined(_WINDOWS64) && !defined(MINECRAFT_SERVER_BUILD)
const Win64LceLive::Snapshot snapshot = Win64LceLive::GetSnapshot();
switch (snapshot.state)
{
case Win64LceLive::EClientState::SignedIn:
buttonLabel = L"SIGN OUT";
buttonEnabled = !snapshot.requestInFlight;
break;
case Win64LceLive::EClientState::StartingLink:
case Win64LceLive::EClientState::Polling:
buttonLabel = L"PLEASE WAIT";
buttonEnabled = false;
break;
case Win64LceLive::EClientState::LinkPending:
buttonLabel = L"RESTART LINK";
buttonEnabled = true;
break;
case Win64LceLive::EClientState::SignedOut:
default:
buttonLabel = L"START LINK";
buttonEnabled = true;
break;
}
description = snapshot.statusMessage;
if (snapshot.hasError && !snapshot.errorMessage.empty())
{
description += L"\r\nError:\r\n";
description += snapshot.errorMessage;
}
#endif
if (force || m_lastButtonLabel != buttonLabel)
{
m_lastButtonLabel = buttonLabel;
m_buttonPrimaryAction.setLabel(buttonLabel, true, true);
}
if (force || m_buttonEnabled != buttonEnabled)
{
m_buttonEnabled = buttonEnabled;
m_buttonPrimaryAction.setEnable(buttonEnabled);
}
if (!m_descriptionApplied || m_lastDescription != description)
{
m_lastDescription = description;
ApplyDescription(description);
}
updateTooltips();
}
void UIScene_LceLive::ApplyDescription(const std::wstring &description)
{
m_labelDescription.setLabel(description, true, true);
IggyDataValue result;
IggyPlayerCallMethodRS(getMovie(), &result, IggyPlayerRootPath(getMovie()), m_funcAutoResize, 0, nullptr);
m_descriptionApplied = true;
}

View File

@@ -0,0 +1,53 @@
#pragma once
#include "UIScene.h"
class UIScene_LceLive : public UIScene
{
private:
enum EControls
{
eControl_PrimaryAction,
};
bool m_buttonEnabled;
bool m_descriptionApplied;
std::wstring m_lastButtonLabel;
std::wstring m_lastDescription;
UIControl_Button m_buttonPrimaryAction;
UIControl_Label m_labelTitle;
UIControl_Label m_labelDescription;
IggyName m_funcInit;
IggyName m_funcAutoResize;
UI_BEGIN_MAP_ELEMENTS_AND_NAMES(UIScene)
UI_MAP_ELEMENT(m_buttonPrimaryAction, "Button3")
UI_MAP_ELEMENT(m_labelTitle, "Title")
UI_MAP_ELEMENT(m_labelDescription, "Content")
UI_MAP_NAME(m_funcInit, L"Init")
UI_MAP_NAME(m_funcAutoResize, L"AutoResize")
UI_END_MAP_ELEMENTS_AND_NAMES()
public:
UIScene_LceLive(int iPad, void *initData, UILayer *parentLayer);
~UIScene_LceLive();
virtual EUIScene getSceneType() { return eUIScene_LceLive; }
virtual void updateTooltips();
virtual void updateComponents();
virtual void tick();
protected:
virtual wstring getMoviePath();
virtual void handleReload();
public:
virtual void handleInput(int iPad, int key, bool repeat, bool pressed, bool released, bool &handled);
protected:
void handlePress(F64 controlId, F64 childId);
private:
void RefreshUi(bool force);
void ApplyDescription(const std::wstring &description);
};

View File

@@ -41,7 +41,7 @@ UIScene_MainMenu::UIScene_MainMenu(int iPad, void *initData, UILayer *parentLaye
#endif
m_buttons[static_cast<int>(eControl_Leaderboards)].init(IDS_LEADERBOARDS,eControl_Leaderboards);
m_buttons[static_cast<int>(eControl_Achievements)].init( (UIString)IDS_ACHIEVEMENTS,eControl_Achievements);
m_buttons[static_cast<int>(eControl_Achievements)].init((UIString)IDS_ACHIEVEMENTS, eControl_Achievements);
m_buttons[static_cast<int>(eControl_HelpAndOptions)].init(IDS_HELP_AND_OPTIONS,eControl_HelpAndOptions);
if(ProfileManager.IsFullVersion())
{
@@ -54,6 +54,13 @@ UIScene_MainMenu::UIScene_MainMenu(int iPad, void *initData, UILayer *parentLaye
m_buttons[static_cast<int>(eControl_UnlockOrDLC)].init(IDS_UNLOCK_FULL_GAME,eControl_UnlockOrDLC);
}
#if defined(_WINDOWS64) && !defined(MINECRAFT_SERVER_BUILD)
m_buttons[static_cast<int>(eControl_LceLive)].init(L"LCELIVE", eControl_LceLive);
#else
m_buttons[static_cast<int>(eControl_LceLive)].init(L"LCELIVE", eControl_LceLive);
removeControl(&m_buttons[(int)eControl_LceLive], false);
#endif
#ifndef _DURANGO
m_buttons[static_cast<int>(eControl_Exit)].init(app.GetString(IDS_EXIT_GAME),eControl_Exit);
#else
@@ -65,11 +72,13 @@ UIScene_MainMenu::UIScene_MainMenu(int iPad, void *initData, UILayer *parentLaye
removeControl( &m_buttons[(int)eControl_Exit], false );
// We don't have a way to display trophies/achievements, so remove the button
removeControl( &m_buttons[(int)eControl_Achievements], false );
removeControl( &m_buttons[(int)eControl_LceLive], false );
m_bLaunchFullVersionPurchase=false;
#endif
#ifdef _DURANGO
// Allowed to not have achievements in the menu
removeControl( &m_buttons[(int)eControl_Achievements], false );
removeControl( &m_buttons[(int)eControl_LceLive], false );
// Not allowed to exit from a Xbox One game from the game - have to use the Home button
//removeControl( &m_buttons[(int)eControl_Exit], false );
m_bWaitingForDLCInfo=false;
@@ -237,10 +246,14 @@ void UIScene_MainMenu::handleReload()
removeControl( &m_buttons[(int)eControl_Exit], false );
// We don't have a way to display trophies/achievements, so remove the button
removeControl( &m_buttons[(int)eControl_Achievements], false );
removeControl( &m_buttons[(int)eControl_LceLive], false );
#endif
#ifdef _DURANGO
// Allowed to not have achievements in the menu
removeControl( &m_buttons[(int)eControl_Achievements], false );
removeControl( &m_buttons[(int)eControl_LceLive], false );
#elif !defined(_WINDOWS64) || defined(MINECRAFT_SERVER_BUILD)
removeControl( &m_buttons[(int)eControl_LceLive], false );
#endif
}
@@ -341,10 +354,16 @@ void UIScene_MainMenu::handlePress(F64 controlId, F64 childId)
case eControl_Achievements:
//CD - Added for audio
ui.PlayUISFX(eSFX_Press);
m_eAction=eAction_RunAchievements;
signInReturnedFunc = &UIScene_MainMenu::Achievements_SignInReturned;
break;
case eControl_LceLive:
//CD - Added for audio
ui.PlayUISFX(eSFX_Press);
#if defined(_WINDOWS64) && !defined(MINECRAFT_SERVER_BUILD)
ui.NavigateToScene(m_iPad, eUIScene_LceLive);
#endif
return;
case eControl_HelpAndOptions:
//CD - Added for audio
ui.PlayUISFX(eSFX_Press);

View File

@@ -12,6 +12,7 @@ private:
eControl_Achievements,
eControl_HelpAndOptions,
eControl_UnlockOrDLC,
eControl_LceLive,
#ifndef _DURANGO
eControl_Exit,
#else
@@ -37,6 +38,7 @@ private:
UI_MAP_ELEMENT( m_buttons[(int)eControl_Achievements], "Button3")
UI_MAP_ELEMENT( m_buttons[(int)eControl_HelpAndOptions], "Button4")
UI_MAP_ELEMENT( m_buttons[(int)eControl_UnlockOrDLC], "Button5")
UI_MAP_ELEMENT( m_buttons[(int)eControl_LceLive], "Button7")
#ifndef _DURANGO
UI_MAP_ELEMENT( m_buttons[(int)eControl_Exit], "Button6")
#else

View File

@@ -0,0 +1,973 @@
#include "stdafx.h"
#ifdef _WINDOWS64
#include "Windows64_LceLive.h"
#include "Windows64_Xuid.h"
#include "../../Minecraft.World/StringHelpers.h"
#include "../../Minecraft.Server/vendor/nlohmann/json.hpp"
#include <Windows.h>
#include <wincrypt.h>
#include <winhttp.h>
#include <cstdio>
#include <string>
#include <vector>
namespace
{
using Json = nlohmann::json;
enum class ERequestType
{
None,
StartLink,
Poll,
Refresh,
Logout,
};
struct AuthSession
{
bool valid;
std::string accountId;
std::string username;
std::string displayName;
std::string accessToken;
std::string refreshToken;
};
struct PendingLink
{
bool active;
std::string deviceCode;
std::string userCode;
std::string verificationUri;
std::string verificationUriComplete;
ULONGLONG nextPollAt;
};
struct RequestContext
{
ERequestType type;
std::string path;
std::string body;
};
struct CompletedRequest
{
ERequestType type;
bool transportOk;
DWORD httpStatus;
std::string responseBody;
};
struct RuntimeState
{
bool initialized;
CRITICAL_SECTION lock;
bool requestInFlight;
bool completedReady;
bool sessionRefreshInFlight;
HANDLE workerThread;
RequestContext request;
CompletedRequest completed;
AuthSession session;
PendingLink pendingLink;
std::string lastError;
};
RuntimeState g_state = {};
INIT_ONCE g_initializeOnce = INIT_ONCE_STATIC_INIT;
INIT_ONCE g_authBlobPathOnce = INIT_ONCE_STATIC_INIT;
char g_authBlobPath[MAX_PATH] = {};
bool BuildExeRelativePath(const char *fileName, char *outPath, size_t outPathSize)
{
if (fileName == nullptr || outPath == nullptr || outPathSize == 0)
return false;
outPath[0] = 0;
char exePath[MAX_PATH] = {};
DWORD len = GetModuleFileNameA(nullptr, exePath, MAX_PATH);
if (len == 0 || len >= MAX_PATH)
return false;
char *lastSlash = strrchr(exePath, '\\');
if (lastSlash != nullptr)
*(lastSlash + 1) = 0;
if (strcpy_s(outPath, outPathSize, exePath) != 0)
return false;
if (strcat_s(outPath, outPathSize, fileName) != 0)
return false;
return true;
}
std::wstring Utf8ToWide(const std::string &text)
{
if (text.empty())
return L"";
const int length = MultiByteToWideChar(CP_UTF8, 0, text.c_str(), static_cast<int>(text.size()), nullptr, 0);
if (length <= 0)
return convStringToWstring(text);
std::wstring result;
result.resize(static_cast<size_t>(length));
MultiByteToWideChar(CP_UTF8, 0, text.c_str(), static_cast<int>(text.size()), &result[0], length);
return result;
}
std::string WideToUtf8(const std::wstring &text)
{
if (text.empty())
return std::string();
const int length = WideCharToMultiByte(CP_UTF8, 0, text.c_str(), static_cast<int>(text.size()), nullptr, 0, nullptr, nullptr);
if (length <= 0)
return std::string(wstringtochararray(text));
std::string result;
result.resize(static_cast<size_t>(length));
WideCharToMultiByte(CP_UTF8, 0, text.c_str(), static_cast<int>(text.size()), &result[0], length, nullptr, nullptr);
return result;
}
std::string JsonStringOrEmpty(const Json &object, const char *key)
{
const Json::const_iterator it = object.find(key);
if (it == object.end() || !it->is_string())
return std::string();
return it->get<std::string>();
}
std::string ParseErrorMessage(const std::string &responseBody, const std::string &fallback);
void TrimTrailingSlashes(std::string *value)
{
if (value == nullptr)
return;
while (!value->empty() && (value->back() == '/' || value->back() == '\\' || value->back() == '\r' || value->back() == '\n' || value->back() == ' ' || value->back() == '\t'))
value->pop_back();
}
std::string BuildDeviceId()
{
const unsigned long long xuid = static_cast<unsigned long long>(Win64Xuid::ResolvePersistentXuid());
char buffer[64] = {};
sprintf_s(buffer, "win64-%016llX", xuid);
return buffer;
}
std::string BuildDeviceName()
{
wchar_t computerName[MAX_COMPUTERNAME_LENGTH + 1] = {};
DWORD size = MAX_COMPUTERNAME_LENGTH + 1;
if (GetComputerNameW(computerName, &size))
{
const std::wstring displayName = L"MCLCE Windows64 (" + std::wstring(computerName) + L")";
return WideToUtf8(displayName);
}
return "MCLCE Windows64";
}
std::string GetApiBaseUrl()
{
char envValue[512] = {};
const DWORD envLength = GetEnvironmentVariableA("LCELIVE_API_BASE_URL", envValue, static_cast<DWORD>(sizeof(envValue)));
if (envLength > 0 && envLength < sizeof(envValue))
{
std::string value(envValue);
TrimTrailingSlashes(&value);
return value;
}
char configPath[MAX_PATH] = {};
if (BuildExeRelativePath("lcelive.properties", configPath, sizeof(configPath)))
{
FILE *file = nullptr;
if (fopen_s(&file, configPath, "rb") == 0 && file != nullptr)
{
char line[512] = {};
while (fgets(line, sizeof(line), file) != nullptr)
{
std::string currentLine(line);
while (!currentLine.empty() &&
(currentLine[currentLine.size() - 1] == '\n' || currentLine[currentLine.size() - 1] == '\r'))
{
currentLine.erase(currentLine.size() - 1);
}
const size_t equalsIndex = currentLine.find('=');
if (equalsIndex == std::string::npos)
continue;
if (currentLine.substr(0, equalsIndex) == "api_base_url")
{
fclose(file);
std::string value = currentLine.substr(equalsIndex + 1);
TrimTrailingSlashes(&value);
return value;
}
}
fclose(file);
}
}
std::string fallback = "http://localhost:5187";
TrimTrailingSlashes(&fallback);
return fallback;
}
std::string BuildHttpFailureMessage(DWORD httpStatus, const std::string &responseBody, const std::string &fallback)
{
const std::string parsed = ParseErrorMessage(responseBody, std::string());
if (!parsed.empty())
return parsed;
switch (httpStatus)
{
case 404:
return "LCELive start request returned HTTP 404. Check the API base URL and port.";
case 500:
return "LCELive API returned HTTP 500 while creating a device link.";
case 502:
case 503:
case 504:
return "LCELive API is unavailable right now.";
default:
break;
}
char buffer[96] = {};
sprintf_s(buffer, "LCELive start request failed with HTTP %lu.", static_cast<unsigned long>(httpStatus));
return fallback.empty() ? std::string(buffer) : std::string(buffer);
}
bool ReadFileBytes(const char *path, std::vector<unsigned char> *outBytes)
{
if (path == nullptr || outBytes == nullptr)
return false;
FILE *file = nullptr;
if (fopen_s(&file, path, "rb") != 0 || file == nullptr)
return false;
if (fseek(file, 0, SEEK_END) != 0)
{
fclose(file);
return false;
}
const long fileSize = ftell(file);
if (fileSize <= 0)
{
fclose(file);
return false;
}
if (fseek(file, 0, SEEK_SET) != 0)
{
fclose(file);
return false;
}
outBytes->resize(static_cast<size_t>(fileSize));
const size_t readCount = fread(outBytes->data(), 1, outBytes->size(), file);
fclose(file);
return readCount == outBytes->size();
}
bool WriteFileBytes(const char *path, const unsigned char *data, size_t size)
{
if (path == nullptr || data == nullptr || size == 0)
return false;
FILE *file = nullptr;
if (fopen_s(&file, path, "wb") != 0 || file == nullptr)
return false;
const size_t writeCount = fwrite(data, 1, size, file);
fclose(file);
return writeCount == size;
}
bool ProtectString(const std::string &plainText, std::vector<unsigned char> *outEncrypted)
{
if (outEncrypted == nullptr)
return false;
DATA_BLOB inputBlob = {};
inputBlob.pbData = reinterpret_cast<BYTE *>(const_cast<char *>(plainText.data()));
inputBlob.cbData = static_cast<DWORD>(plainText.size());
DATA_BLOB outputBlob = {};
if (!CryptProtectData(&inputBlob, L"LCELive", nullptr, nullptr, nullptr, CRYPTPROTECT_UI_FORBIDDEN, &outputBlob))
return false;
outEncrypted->assign(outputBlob.pbData, outputBlob.pbData + outputBlob.cbData);
LocalFree(outputBlob.pbData);
return true;
}
bool UnprotectBytes(const std::vector<unsigned char> &encrypted, std::string *outPlainText)
{
if (outPlainText == nullptr || encrypted.empty())
return false;
DATA_BLOB inputBlob = {};
inputBlob.pbData = const_cast<BYTE *>(encrypted.data());
inputBlob.cbData = static_cast<DWORD>(encrypted.size());
DATA_BLOB outputBlob = {};
if (!CryptUnprotectData(&inputBlob, nullptr, nullptr, nullptr, nullptr, CRYPTPROTECT_UI_FORBIDDEN, &outputBlob))
return false;
outPlainText->assign(reinterpret_cast<const char *>(outputBlob.pbData), reinterpret_cast<const char *>(outputBlob.pbData) + outputBlob.cbData);
LocalFree(outputBlob.pbData);
return true;
}
const char *GetAuthBlobPath()
{
InitOnceExecuteOnce(&g_authBlobPathOnce,
[](PINIT_ONCE, PVOID, PVOID *) -> BOOL
{
BuildExeRelativePath("lcelive_auth.dat", g_authBlobPath, sizeof(g_authBlobPath));
return TRUE;
},
nullptr,
nullptr);
return g_authBlobPath;
}
void SaveAuthSessionLocked()
{
if (!g_state.session.valid)
{
DeleteFileA(GetAuthBlobPath());
return;
}
Json sessionJson;
sessionJson["version"] = 1;
sessionJson["accountId"] = g_state.session.accountId;
sessionJson["username"] = g_state.session.username;
sessionJson["displayName"] = g_state.session.displayName;
sessionJson["accessToken"] = g_state.session.accessToken;
sessionJson["refreshToken"] = g_state.session.refreshToken;
std::vector<unsigned char> encrypted;
if (!ProtectString(sessionJson.dump(), &encrypted))
{
app.DebugPrintf("LCELive: failed to protect auth blob for local storage\n");
return;
}
if (!WriteFileBytes(GetAuthBlobPath(), encrypted.data(), encrypted.size()))
app.DebugPrintf("LCELive: failed to write auth blob to disk\n");
}
void ClearSessionLocked()
{
g_state.session = {};
g_state.pendingLink = {};
g_state.lastError.clear();
DeleteFileA(GetAuthBlobPath());
}
void LoadPersistedSessionLocked()
{
std::vector<unsigned char> encrypted;
if (!ReadFileBytes(GetAuthBlobPath(), &encrypted))
return;
std::string decrypted;
if (!UnprotectBytes(encrypted, &decrypted))
{
app.DebugPrintf("LCELive: unable to decrypt stored auth state, clearing local blob\n");
DeleteFileA(GetAuthBlobPath());
return;
}
const Json sessionJson = Json::parse(decrypted, nullptr, false);
if (!sessionJson.is_object())
{
app.DebugPrintf("LCELive: stored auth state is invalid JSON, clearing local blob\n");
DeleteFileA(GetAuthBlobPath());
return;
}
g_state.session.refreshToken = JsonStringOrEmpty(sessionJson, "refreshToken");
if (g_state.session.refreshToken.empty())
{
DeleteFileA(GetAuthBlobPath());
g_state.session = {};
return;
}
g_state.session.valid = true;
g_state.session.accountId = JsonStringOrEmpty(sessionJson, "accountId");
g_state.session.username = JsonStringOrEmpty(sessionJson, "username");
g_state.session.displayName = JsonStringOrEmpty(sessionJson, "displayName");
g_state.session.accessToken = JsonStringOrEmpty(sessionJson, "accessToken");
}
bool CrackUrl(const std::wstring &baseUrl, URL_COMPONENTSW *outComponents, std::vector<wchar_t> *hostBuffer, std::vector<wchar_t> *pathBuffer)
{
if (outComponents == nullptr || hostBuffer == nullptr || pathBuffer == nullptr)
return false;
hostBuffer->assign(256, 0);
pathBuffer->assign(2048, 0);
ZeroMemory(outComponents, sizeof(*outComponents));
outComponents->dwStructSize = sizeof(*outComponents);
outComponents->lpszHostName = hostBuffer->data();
outComponents->dwHostNameLength = static_cast<DWORD>(hostBuffer->size());
outComponents->lpszUrlPath = pathBuffer->data();
outComponents->dwUrlPathLength = static_cast<DWORD>(pathBuffer->size());
return WinHttpCrackUrl(baseUrl.c_str(), static_cast<DWORD>(baseUrl.length()), 0, outComponents) == TRUE;
}
bool PerformJsonRequest(const RequestContext &request, DWORD *outStatus, std::string *outResponseBody)
{
if (outStatus == nullptr || outResponseBody == nullptr)
return false;
*outStatus = 0;
outResponseBody->clear();
const std::string baseUrlUtf8 = GetApiBaseUrl();
const std::wstring baseUrl = Utf8ToWide(baseUrlUtf8);
URL_COMPONENTSW components = {};
std::vector<wchar_t> hostBuffer;
std::vector<wchar_t> pathBuffer;
if (!CrackUrl(baseUrl, &components, &hostBuffer, &pathBuffer))
{
app.DebugPrintf("LCELive: WinHttpCrackUrl failed for '%s'\n", baseUrlUtf8.c_str());
return false;
}
std::wstring fullPath = components.lpszUrlPath != nullptr
? std::wstring(components.lpszUrlPath, components.dwUrlPathLength)
: std::wstring();
if (!request.path.empty())
fullPath += Utf8ToWide(request.path);
if (fullPath.empty())
fullPath = L"/";
const bool secure = components.nScheme == INTERNET_SCHEME_HTTPS;
HINTERNET session = WinHttpOpen(L"MCLCE-LceLive/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0);
if (session == nullptr)
return false;
WinHttpSetTimeouts(session, 5000, 5000, 10000, 10000);
const std::wstring hostWide(components.lpszHostName, components.dwHostNameLength);
HINTERNET connection = WinHttpConnect(session, hostWide.c_str(), components.nPort, 0);
if (connection == nullptr)
{
WinHttpCloseHandle(session);
return false;
}
const wchar_t *verb = request.body.empty() ? L"GET" : L"POST";
HINTERNET requestHandle = WinHttpOpenRequest(connection, verb, fullPath.c_str(), nullptr, WINHTTP_NO_REFERER, WINHTTP_DEFAULT_ACCEPT_TYPES, secure ? WINHTTP_FLAG_SECURE : 0);
if (requestHandle == nullptr)
{
WinHttpCloseHandle(connection);
WinHttpCloseHandle(session);
return false;
}
const wchar_t *headers = request.body.empty()
? L"Accept: application/json\r\n"
: L"Content-Type: application/json\r\nAccept: application/json\r\n";
LPVOID sendBuffer = WINHTTP_NO_REQUEST_DATA;
DWORD sendSize = 0;
if (!request.body.empty())
{
sendBuffer = const_cast<char *>(request.body.data());
sendSize = static_cast<DWORD>(request.body.size());
}
const BOOL sendOk = WinHttpSendRequest(requestHandle, headers, -1L, sendBuffer, sendSize, sendSize, 0);
if (!sendOk || !WinHttpReceiveResponse(requestHandle, nullptr))
{
WinHttpCloseHandle(requestHandle);
WinHttpCloseHandle(connection);
WinHttpCloseHandle(session);
return false;
}
DWORD statusCode = 0;
DWORD statusCodeSize = sizeof(statusCode);
if (!WinHttpQueryHeaders(requestHandle, WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, WINHTTP_HEADER_NAME_BY_INDEX, &statusCode, &statusCodeSize, WINHTTP_NO_HEADER_INDEX))
{
WinHttpCloseHandle(requestHandle);
WinHttpCloseHandle(connection);
WinHttpCloseHandle(session);
return false;
}
std::string responseBody;
for (;;)
{
DWORD bytesAvailable = 0;
if (!WinHttpQueryDataAvailable(requestHandle, &bytesAvailable))
break;
if (bytesAvailable == 0)
break;
std::vector<char> buffer(bytesAvailable);
DWORD bytesRead = 0;
if (!WinHttpReadData(requestHandle, buffer.data(), bytesAvailable, &bytesRead))
break;
responseBody.append(buffer.data(), buffer.data() + bytesRead);
}
*outStatus = statusCode;
*outResponseBody = responseBody;
WinHttpCloseHandle(requestHandle);
WinHttpCloseHandle(connection);
WinHttpCloseHandle(session);
return true;
}
std::string ParseErrorMessage(const std::string &responseBody, const std::string &fallback)
{
if (responseBody.empty())
return fallback;
const Json responseJson = Json::parse(responseBody, nullptr, false);
if (!responseJson.is_object())
return fallback;
const std::string message = JsonStringOrEmpty(responseJson, "message");
return message.empty() ? fallback : message;
}
DWORD WINAPI RequestThreadProc(LPVOID)
{
RequestContext request = {};
EnterCriticalSection(&g_state.lock);
request = g_state.request;
LeaveCriticalSection(&g_state.lock);
CompletedRequest completed = {};
completed.type = request.type;
completed.transportOk = PerformJsonRequest(request, &completed.httpStatus, &completed.responseBody);
EnterCriticalSection(&g_state.lock);
g_state.completed = completed;
g_state.completedReady = true;
g_state.requestInFlight = false;
LeaveCriticalSection(&g_state.lock);
return 0;
}
bool QueueRequestLocked(ERequestType type, const std::string &path, const std::string &body)
{
if (g_state.requestInFlight)
return false;
if (g_state.workerThread != nullptr)
{
WaitForSingleObject(g_state.workerThread, INFINITE);
CloseHandle(g_state.workerThread);
g_state.workerThread = nullptr;
}
g_state.request = {};
g_state.request.type = type;
g_state.request.path = path;
g_state.request.body = body;
g_state.requestInFlight = true;
g_state.completedReady = false;
g_state.workerThread = CreateThread(nullptr, 0, &RequestThreadProc, nullptr, 0, nullptr);
if (g_state.workerThread == nullptr)
{
g_state.requestInFlight = false;
g_state.lastError = "Unable to create LCELive worker thread.";
return false;
}
return true;
}
void QueueSessionRefreshLocked()
{
if (!g_state.session.valid || g_state.session.refreshToken.empty() || g_state.requestInFlight)
return;
Json requestJson;
requestJson["refreshToken"] = g_state.session.refreshToken;
if (QueueRequestLocked(ERequestType::Refresh, "/api/auth/refresh", requestJson.dump()))
g_state.sessionRefreshInFlight = true;
}
BOOL CALLBACK InitializeRuntimeState(PINIT_ONCE, PVOID, PVOID *)
{
InitializeCriticalSection(&g_state.lock);
EnterCriticalSection(&g_state.lock);
g_state.initialized = true;
LoadPersistedSessionLocked();
QueueSessionRefreshLocked();
LeaveCriticalSection(&g_state.lock);
return TRUE;
}
void EnsureInitialized()
{
InitOnceExecuteOnce(&g_initializeOnce, &InitializeRuntimeState, nullptr, nullptr);
}
void ApplyStartLinkResponseLocked(const CompletedRequest &completed)
{
if (!completed.transportOk)
{
g_state.lastError = "Unable to contact LCELive.";
return;
}
if (completed.httpStatus < 200 || completed.httpStatus >= 300)
{
g_state.lastError = BuildHttpFailureMessage(completed.httpStatus, completed.responseBody, "LCELive rejected the device-link request.");
return;
}
const Json responseJson = Json::parse(completed.responseBody, nullptr, false);
if (!responseJson.is_object())
{
g_state.lastError = "LCELive returned an invalid device-link response.";
return;
}
g_state.pendingLink = {};
g_state.pendingLink.active = true;
g_state.pendingLink.deviceCode = JsonStringOrEmpty(responseJson, "deviceCode");
g_state.pendingLink.userCode = JsonStringOrEmpty(responseJson, "userCode");
g_state.pendingLink.verificationUri = JsonStringOrEmpty(responseJson, "verificationUri");
g_state.pendingLink.verificationUriComplete = JsonStringOrEmpty(responseJson, "verificationUriComplete");
int intervalSeconds = 5;
const Json::const_iterator intervalIt = responseJson.find("intervalSeconds");
if (intervalIt != responseJson.end() && intervalIt->is_number_integer())
intervalSeconds = intervalIt->get<int>();
if (intervalSeconds < 1)
intervalSeconds = 5;
g_state.pendingLink.nextPollAt = GetTickCount64() + (static_cast<ULONGLONG>(intervalSeconds) * 1000ULL);
g_state.lastError.clear();
}
void ApplyPollResponseLocked(const CompletedRequest &completed)
{
if (!g_state.pendingLink.active)
return;
if (!completed.transportOk)
{
g_state.pendingLink.active = false;
g_state.lastError = "LCELive polling failed. Press A to request a new code.";
return;
}
if (completed.httpStatus < 200 || completed.httpStatus >= 300)
{
g_state.pendingLink.active = false;
g_state.lastError = ParseErrorMessage(completed.responseBody, "LCELive rejected the device poll.");
return;
}
const Json responseJson = Json::parse(completed.responseBody, nullptr, false);
if (!responseJson.is_object())
{
g_state.pendingLink.active = false;
g_state.lastError = "LCELive returned an invalid poll response.";
return;
}
const std::string status = JsonStringOrEmpty(responseJson, "status");
const bool isLinked = responseJson.contains("isLinked") && responseJson["isLinked"].is_boolean() && responseJson["isLinked"].get<bool>();
if (isLinked)
{
const Json::const_iterator accountIt = responseJson.find("account");
if (accountIt == responseJson.end() || !accountIt->is_object())
{
g_state.pendingLink.active = false;
g_state.lastError = "LCELive linked the device but omitted account details.";
return;
}
g_state.session.valid = true;
g_state.session.accountId = JsonStringOrEmpty(*accountIt, "accountId");
g_state.session.username = JsonStringOrEmpty(*accountIt, "username");
g_state.session.displayName = JsonStringOrEmpty(*accountIt, "displayName");
g_state.session.accessToken = JsonStringOrEmpty(responseJson, "accessToken");
g_state.session.refreshToken = JsonStringOrEmpty(responseJson, "refreshToken");
g_state.pendingLink = {};
g_state.lastError.clear();
SaveAuthSessionLocked();
return;
}
if (status == "pending")
{
g_state.pendingLink.nextPollAt = GetTickCount64() + 5000ULL;
g_state.lastError.clear();
return;
}
g_state.pendingLink.active = false;
if (status == "expired")
g_state.lastError = "That device code expired. Press A to request a new one.";
else
g_state.lastError = "LCELive returned an unexpected poll status.";
}
void ApplyRefreshResponseLocked(const CompletedRequest &completed)
{
g_state.sessionRefreshInFlight = false;
if (!g_state.session.valid)
return;
if (!completed.transportOk)
{
g_state.lastError = "Unable to reach LCELive. Using the cached local sign-in for now.";
return;
}
if (completed.httpStatus < 200 || completed.httpStatus >= 300)
{
ClearSessionLocked();
g_state.lastError = "Saved LCELive sign-in expired. Press A to link this device again.";
return;
}
const Json responseJson = Json::parse(completed.responseBody, nullptr, false);
if (!responseJson.is_object())
{
g_state.lastError = "LCELive returned an invalid session refresh response.";
return;
}
const Json::const_iterator accountIt = responseJson.find("account");
if (accountIt == responseJson.end() || !accountIt->is_object())
{
ClearSessionLocked();
g_state.lastError = "LCELive refresh omitted account details. Link this device again.";
return;
}
const std::string refreshedAccessToken = JsonStringOrEmpty(responseJson, "accessToken");
const std::string refreshedRefreshToken = JsonStringOrEmpty(responseJson, "refreshToken");
if (refreshedAccessToken.empty() || refreshedRefreshToken.empty())
{
ClearSessionLocked();
g_state.lastError = "LCELive refresh returned incomplete credentials. Link this device again.";
return;
}
g_state.session.valid = true;
g_state.session.accountId = JsonStringOrEmpty(*accountIt, "accountId");
g_state.session.username = JsonStringOrEmpty(*accountIt, "username");
g_state.session.displayName = JsonStringOrEmpty(*accountIt, "displayName");
g_state.session.accessToken = refreshedAccessToken;
g_state.session.refreshToken = refreshedRefreshToken;
g_state.lastError.clear();
SaveAuthSessionLocked();
}
void IntegrateCompletedRequestLocked()
{
if (!g_state.completedReady)
return;
const CompletedRequest completed = g_state.completed;
g_state.completedReady = false;
if (g_state.workerThread != nullptr)
{
WaitForSingleObject(g_state.workerThread, INFINITE);
CloseHandle(g_state.workerThread);
g_state.workerThread = nullptr;
}
switch (completed.type)
{
case ERequestType::StartLink:
ApplyStartLinkResponseLocked(completed);
break;
case ERequestType::Poll:
ApplyPollResponseLocked(completed);
break;
case ERequestType::Refresh:
ApplyRefreshResponseLocked(completed);
break;
case ERequestType::Logout:
default:
break;
}
}
std::wstring BuildStatusMessageLocked()
{
if (g_state.session.valid)
{
std::wstring message;
message += L"Signed in as:\r\n";
message += Utf8ToWide(g_state.session.displayName);
if (!g_state.session.username.empty())
{
message += L" (@";
message += Utf8ToWide(g_state.session.username);
message += L")";
}
message += L"\r\nThis device is linked locally for LCELive features.";
if (g_state.sessionRefreshInFlight)
message += L"\r\nRefreshing the saved session with LCELive...";
message += L"\r\nPress A to sign out on this device.";
return message;
}
if (g_state.pendingLink.active)
{
std::wstring message;
message += L"Go to:\r\n";
message += Utf8ToWide(g_state.pendingLink.verificationUri);
message += L"\r\nEnter code:\r\n";
message += Utf8ToWide(g_state.pendingLink.userCode);
message += L"\r\nThis screen will keep checking until the link completes.";
return message;
}
return L"Sign-in is optional. Offline play is unchanged.\r\nPress A to request a device code.\r\nThen visit the LCELive link page and enter the code shown here.";
}
}
namespace Win64LceLive
{
void Tick()
{
EnsureInitialized();
EnterCriticalSection(&g_state.lock);
IntegrateCompletedRequestLocked();
if (g_state.pendingLink.active && !g_state.requestInFlight && GetTickCount64() >= g_state.pendingLink.nextPollAt)
QueueRequestLocked(ERequestType::Poll, "/api/auth/device/poll/" + g_state.pendingLink.deviceCode, std::string());
LeaveCriticalSection(&g_state.lock);
}
Snapshot GetSnapshot()
{
EnsureInitialized();
Tick();
Snapshot snapshot = {};
EnterCriticalSection(&g_state.lock);
snapshot.requestInFlight = g_state.requestInFlight;
snapshot.hasError = !g_state.lastError.empty();
snapshot.errorMessage = Utf8ToWide(g_state.lastError);
snapshot.statusMessage = BuildStatusMessageLocked();
if (g_state.session.valid)
{
snapshot.state = EClientState::SignedIn;
snapshot.accountDisplayName = Utf8ToWide(g_state.session.displayName);
snapshot.accountUsername = Utf8ToWide(g_state.session.username);
snapshot.accountId = Utf8ToWide(g_state.session.accountId);
}
else if (g_state.pendingLink.active)
{
snapshot.state = g_state.requestInFlight ? EClientState::Polling : EClientState::LinkPending;
snapshot.verificationUri = Utf8ToWide(g_state.pendingLink.verificationUri);
snapshot.verificationUriComplete = Utf8ToWide(g_state.pendingLink.verificationUriComplete);
snapshot.userCode = Utf8ToWide(g_state.pendingLink.userCode);
}
else if (g_state.requestInFlight)
{
snapshot.state = EClientState::StartingLink;
}
else
{
snapshot.state = EClientState::SignedOut;
}
LeaveCriticalSection(&g_state.lock);
return snapshot;
}
bool StartDeviceLink()
{
EnsureInitialized();
EnterCriticalSection(&g_state.lock);
IntegrateCompletedRequestLocked();
if (g_state.requestInFlight || g_state.session.valid)
{
LeaveCriticalSection(&g_state.lock);
return false;
}
g_state.pendingLink = {};
g_state.lastError.clear();
Json requestJson;
requestJson["deviceId"] = BuildDeviceId();
requestJson["deviceName"] = BuildDeviceName();
const bool queued = QueueRequestLocked(ERequestType::StartLink, "/api/auth/device/start", requestJson.dump());
LeaveCriticalSection(&g_state.lock);
return queued;
}
bool SignOut()
{
EnsureInitialized();
EnterCriticalSection(&g_state.lock);
IntegrateCompletedRequestLocked();
if (g_state.requestInFlight)
{
LeaveCriticalSection(&g_state.lock);
return false;
}
if (!g_state.session.valid && !g_state.pendingLink.active)
{
LeaveCriticalSection(&g_state.lock);
return false;
}
const std::string refreshToken = g_state.session.refreshToken;
ClearSessionLocked();
if (!refreshToken.empty() && !g_state.requestInFlight)
{
Json requestJson;
requestJson["refreshToken"] = refreshToken;
QueueRequestLocked(ERequestType::Logout, "/api/auth/logout", requestJson.dump());
}
LeaveCriticalSection(&g_state.lock);
return true;
}
}
#endif

View File

@@ -0,0 +1,39 @@
#pragma once
#ifdef _WINDOWS64
#include <string>
namespace Win64LceLive
{
enum class EClientState
{
SignedOut,
StartingLink,
LinkPending,
Polling,
SignedIn,
};
struct Snapshot
{
EClientState state;
bool requestInFlight;
bool hasError;
std::wstring accountDisplayName;
std::wstring accountUsername;
std::wstring accountId;
std::wstring verificationUri;
std::wstring verificationUriComplete;
std::wstring userCode;
std::wstring statusMessage;
std::wstring errorMessage;
};
void Tick();
Snapshot GetSnapshot();
bool StartDeviceLink();
bool SignOut();
}
#endif

View File

@@ -150,6 +150,8 @@ set(_MINECRAFT_CLIENT_WINDOWS_COMMON_UI_SCENES_FRONTEND_MENU_SCREENS
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_Intro.h"
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_JoinMenu.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_JoinMenu.h"
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_LceLive.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_LceLive.h"
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_LaunchMoreOptionsMenu.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_LaunchMoreOptionsMenu.h"
"${CMAKE_CURRENT_SOURCE_DIR}/Common/UI/UIScene_LeaderboardsMenu.cpp"
@@ -340,6 +342,8 @@ set(_MINECRAFT_CLIENT_WINDOWS_WINDOWS64
"${BASE_DIR}/Resource.h"
"${BASE_DIR}/Windows64_App.cpp"
"${BASE_DIR}/Windows64_App.h"
"${BASE_DIR}/Windows64_LceLive.cpp"
"${BASE_DIR}/Windows64_LceLive.h"
"${BASE_DIR}/Windows64_UIController.cpp"
"${BASE_DIR}/Windows64_UIController.h"
"${BASE_DIR}/KeyboardMouseInput.cpp"

View File

@@ -37,9 +37,11 @@ set_target_properties(Minecraft.Server PROPERTIES
target_link_libraries(Minecraft.Server PRIVATE
Minecraft.World
crypt32
d3d11
d3dcompiler
XInput9_1_0
winhttp
wsock32
legacy_stdio_definitions
$<$<CONFIG:Debug>: # Debug 4J libraries

View File

@@ -208,6 +208,7 @@ set(_MINECRAFT_SERVER_COMMON_ROOT
"${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_InventoryMenu.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_JoinMenu.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_Keyboard.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_LceLive.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_LanguageSelector.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_LaunchMoreOptionsMenu.cpp"
"${CMAKE_CURRENT_SOURCE_DIR}/../Minecraft.Client/Common/UI/UIScene_LeaderboardsMenu.cpp"

150
tools/repack_media_arc.py Normal file
View File

@@ -0,0 +1,150 @@
#!/usr/bin/env python3
from __future__ import annotations
import argparse
import shutil
import struct
from dataclasses import dataclass
from pathlib import Path
@dataclass
class Entry:
raw_name: str
ptr: int
size: int
data: bytes
@property
def logical_name(self) -> str:
return self.raw_name[1:] if self.raw_name.startswith("*") else self.raw_name
def read_utf(data: bytes, offset: int) -> tuple[str, int]:
(utf_len,) = struct.unpack_from(">H", data, offset)
offset += 2
raw = data[offset : offset + utf_len]
offset += utf_len
return raw.decode("utf-8"), offset
def write_utf(value: str) -> bytes:
raw = value.encode("utf-8")
if len(raw) > 0xFFFF:
raise ValueError(f"archive entry name is too long: {value}")
return struct.pack(">H", len(raw)) + raw
def load_archive(path: Path) -> list[Entry]:
blob = path.read_bytes()
offset = 0
(count,) = struct.unpack_from(">i", blob, offset)
offset += 4
headers: list[tuple[str, int, int]] = []
for _ in range(count):
raw_name, offset = read_utf(blob, offset)
ptr, size = struct.unpack_from(">ii", blob, offset)
offset += 8
headers.append((raw_name, ptr, size))
entries: list[Entry] = []
for raw_name, ptr, size in headers:
entries.append(Entry(raw_name=raw_name, ptr=ptr, size=size, data=blob[ptr : ptr + size]))
return entries
def build_archive(entries: list[Entry]) -> bytes:
header_parts: list[bytes] = [struct.pack(">i", len(entries))]
data_offset = 4
encoded_names = [write_utf(entry.raw_name) for entry in entries]
for encoded_name, entry in zip(encoded_names, entries, strict=True):
data_offset += len(encoded_name) + 8
payload_parts: list[bytes] = []
current_ptr = data_offset
for encoded_name, entry in zip(encoded_names, entries, strict=True):
header_parts.append(encoded_name)
header_parts.append(struct.pack(">ii", current_ptr, len(entry.data)))
payload_parts.append(entry.data)
current_ptr += len(entry.data)
return b"".join(header_parts + payload_parts)
def apply_overlays(entries: list[Entry], overlays: dict[str, Path]) -> list[str]:
replaced: list[str] = []
by_name = {entry.logical_name: entry for entry in entries}
for logical_name, overlay_path in overlays.items():
if not overlay_path.exists():
raise FileNotFoundError(f"overlay file does not exist: {overlay_path}")
overlay_data = overlay_path.read_bytes()
existing = by_name.get(logical_name)
if existing is None:
entries.append(Entry(raw_name=logical_name, ptr=0, size=len(overlay_data), data=overlay_data))
else:
if existing.raw_name.startswith("*"):
raise ValueError(
f"refusing to replace compressed archive entry '{existing.raw_name}' with raw data"
)
existing.data = overlay_data
existing.size = len(overlay_data)
replaced.append(logical_name)
return replaced
def parse_overlay_args(values: list[str]) -> dict[str, Path]:
overlays: dict[str, Path] = {}
for value in values:
name, separator, path = value.partition("=")
if not separator:
raise ValueError(f"overlay must be NAME=PATH, got: {value}")
overlays[name] = Path(path)
return overlays
def main() -> int:
parser = argparse.ArgumentParser(description="Overlay selected files into a MediaWindows64.arc archive.")
parser.add_argument("input", type=Path, help="Existing archive to read")
parser.add_argument("output", type=Path, help="Archive path to write")
parser.add_argument(
"--overlay",
action="append",
default=[],
metavar="NAME=PATH",
help="Replace or add archive entry NAME with the bytes from PATH",
)
parser.add_argument(
"--backup",
action="store_true",
help="Write a .bak copy of the output file before replacing it when the output already exists",
)
args = parser.parse_args()
overlays = parse_overlay_args(args.overlay)
entries = load_archive(args.input)
touched = apply_overlays(entries, overlays)
rebuilt = build_archive(entries)
args.output.parent.mkdir(parents=True, exist_ok=True)
if args.backup and args.output.exists():
backup_path = args.output.with_suffix(args.output.suffix + ".bak")
shutil.copy2(args.output, backup_path)
args.output.write_bytes(rebuilt)
print(f"wrote {args.output}")
print(f"entries: {len(entries)}")
if touched:
print("overlays:")
for logical_name in touched:
print(f" {logical_name}")
return 0
if __name__ == "__main__":
raise SystemExit(main())