diff --git a/Minecraft.Client/CMakeLists.txt b/Minecraft.Client/CMakeLists.txt index 9f75efd2..abe39f18 100644 --- a/Minecraft.Client/CMakeLists.txt +++ b/Minecraft.Client/CMakeLists.txt @@ -52,6 +52,7 @@ set_target_properties(Minecraft.Client PROPERTIES target_link_libraries(Minecraft.Client PRIVATE Minecraft.World d3d11 + dxgi d3dcompiler XInput9_1_0 wsock32 diff --git a/Minecraft.Client/Common/App_Defines.h b/Minecraft.Client/Common/App_Defines.h index 332e1e73..8349be8c 100644 --- a/Minecraft.Client/Common/App_Defines.h +++ b/Minecraft.Client/Common/App_Defines.h @@ -105,6 +105,8 @@ enum EGameHostOptionWorldSize #define GAMESETTING_ANIMATEDCHARACTER 0x00008000 #define GAMESETTING_PS3EULAREAD 0x00010000 #define GAMESETTING_PSVITANETWORKMODEADHOC 0x00020000 +#define GAMESETTING_VSYNC 0x00040000 +#define GAMESETTING_EXCLUSIVEFULLSCREEN 0x00080000 // defines for languages diff --git a/Minecraft.Client/Common/App_enums.h b/Minecraft.Client/Common/App_enums.h index 0fbcd21b..70d157b0 100644 --- a/Minecraft.Client/Common/App_enums.h +++ b/Minecraft.Client/Common/App_enums.h @@ -178,6 +178,9 @@ enum eGameSetting // PSVita eGameSetting_PSVita_NetworkModeAdhoc, + // PC + eGameSetting_VSync, + eGameSetting_ExclusiveFullscreen, }; diff --git a/Minecraft.Client/Common/Consoles_App.cpp b/Minecraft.Client/Common/Consoles_App.cpp index bd0ef11b..1e038de6 100644 --- a/Minecraft.Client/Common/Consoles_App.cpp +++ b/Minecraft.Client/Common/Consoles_App.cpp @@ -1382,6 +1382,7 @@ void CMinecraftApp::ApplyGameSettingsChanged(int iPad) ActionGameSettings(iPad,eGameSetting_AnimatedCharacter); ActionGameSettings(iPad,eGameSetting_PS3_EULA_Read); + ActionGameSettings(iPad,eGameSetting_VSync); } @@ -1617,6 +1618,22 @@ void CMinecraftApp::ActionGameSettings(int iPad,eGameSetting eVal) case eGameSetting_PSVita_NetworkModeAdhoc: //nothing to do here break; + case eGameSetting_VSync: +#ifdef _WINDOWS64 + { + extern bool g_bVSync; + g_bVSync = (GetGameSettings(iPad, eGameSetting_VSync) != 0); + } +#endif + break; + case eGameSetting_ExclusiveFullscreen: +#ifdef _WINDOWS64 + { + extern void SetExclusiveFullscreen(bool enabled); + SetExclusiveFullscreen(GetGameSettings(iPad, eGameSetting_ExclusiveFullscreen) != 0); + } +#endif + break; } } @@ -2328,6 +2345,38 @@ void CMinecraftApp::SetGameSettings(int iPad,eGameSetting eVal,unsigned char ucV } break; + case eGameSetting_VSync: + if(((GameSettingsA[iPad]->uiBitmaskValues&GAMESETTING_VSYNC)>>18)!=(ucVal&0x01)) + { + if(ucVal==1) + { + GameSettingsA[iPad]->uiBitmaskValues|=GAMESETTING_VSYNC; + } + else + { + GameSettingsA[iPad]->uiBitmaskValues&=~GAMESETTING_VSYNC; + } + ActionGameSettings(iPad,eVal); + GameSettingsA[iPad]->bSettingsChanged=true; + } + break; + + case eGameSetting_ExclusiveFullscreen: + if(((GameSettingsA[iPad]->uiBitmaskValues&GAMESETTING_EXCLUSIVEFULLSCREEN)>>19)!=(ucVal&0x01)) + { + if(ucVal==1) + { + GameSettingsA[iPad]->uiBitmaskValues|=GAMESETTING_EXCLUSIVEFULLSCREEN; + } + else + { + GameSettingsA[iPad]->uiBitmaskValues&=~GAMESETTING_EXCLUSIVEFULLSCREEN; + } + ActionGameSettings(iPad,eVal); + GameSettingsA[iPad]->bSettingsChanged=true; + } + break; + } } @@ -2463,6 +2512,12 @@ unsigned char CMinecraftApp::GetGameSettings(int iPad,eGameSetting eVal) case eGameSetting_PSVita_NetworkModeAdhoc: return (GameSettingsA[iPad]->uiBitmaskValues&GAMESETTING_PSVITANETWORKMODEADHOC)>>17; + case eGameSetting_VSync: + return (GameSettingsA[iPad]->uiBitmaskValues&GAMESETTING_VSYNC)>>18; + + case eGameSetting_ExclusiveFullscreen: + return (GameSettingsA[iPad]->uiBitmaskValues&GAMESETTING_EXCLUSIVEFULLSCREEN)>>19; + } return 0; } diff --git a/Minecraft.Client/Common/Media/MediaWindows64.arc b/Minecraft.Client/Common/Media/MediaWindows64.arc index 4d099532..361a20a5 100644 Binary files a/Minecraft.Client/Common/Media/MediaWindows64.arc and b/Minecraft.Client/Common/Media/MediaWindows64.arc differ diff --git a/Minecraft.Client/Common/Media/SettingsGraphicsMenu1080.swf b/Minecraft.Client/Common/Media/SettingsGraphicsMenu1080.swf index 2495b434..cdf75cff 100644 Binary files a/Minecraft.Client/Common/Media/SettingsGraphicsMenu1080.swf and b/Minecraft.Client/Common/Media/SettingsGraphicsMenu1080.swf differ diff --git a/Minecraft.Client/Common/Media/SettingsGraphicsMenu720.swf b/Minecraft.Client/Common/Media/SettingsGraphicsMenu720.swf index 4e860fb1..19db4f33 100644 Binary files a/Minecraft.Client/Common/Media/SettingsGraphicsMenu720.swf and b/Minecraft.Client/Common/Media/SettingsGraphicsMenu720.swf differ diff --git a/Minecraft.Client/Common/Media/SettingsGraphicsMenuSplit1080.swf b/Minecraft.Client/Common/Media/SettingsGraphicsMenuSplit1080.swf index 6037882d..5f134779 100644 Binary files a/Minecraft.Client/Common/Media/SettingsGraphicsMenuSplit1080.swf and b/Minecraft.Client/Common/Media/SettingsGraphicsMenuSplit1080.swf differ diff --git a/Minecraft.Client/Common/Media/SettingsGraphicsMenuSplit720.swf b/Minecraft.Client/Common/Media/SettingsGraphicsMenuSplit720.swf index 133dae45..842d2123 100644 Binary files a/Minecraft.Client/Common/Media/SettingsGraphicsMenuSplit720.swf and b/Minecraft.Client/Common/Media/SettingsGraphicsMenuSplit720.swf differ diff --git a/Minecraft.Client/Common/UI/UIScene_SettingsGraphicsMenu.cpp b/Minecraft.Client/Common/UI/UIScene_SettingsGraphicsMenu.cpp index b258d8c3..0772e8d4 100644 --- a/Minecraft.Client/Common/UI/UIScene_SettingsGraphicsMenu.cpp +++ b/Minecraft.Client/Common/UI/UIScene_SettingsGraphicsMenu.cpp @@ -5,6 +5,11 @@ #include "..\..\Options.h" #include "..\..\GameRenderer.h" +#ifdef _WINDOWS64 +extern bool g_bVSync; +extern void SetExclusiveFullscreen(bool enabled); +#endif + namespace { constexpr int FOV_MIN = 70; @@ -62,8 +67,10 @@ UIScene_SettingsGraphicsMenu::UIScene_SettingsGraphicsMenu(int iPad, void *initD m_checkboxClouds.init(app.GetString(IDS_CHECKBOX_RENDER_CLOUDS),eControl_Clouds,(app.GetGameSettings(m_iPad,eGameSetting_Clouds)!=0)); m_checkboxBedrockFog.init(app.GetString(IDS_CHECKBOX_RENDER_BEDROCKFOG),eControl_BedrockFog,(app.GetGameSettings(m_iPad,eGameSetting_BedrockFog)!=0)); m_checkboxCustomSkinAnim.init(app.GetString(IDS_CHECKBOX_CUSTOM_SKIN_ANIM),eControl_CustomSkinAnim,(app.GetGameSettings(m_iPad,eGameSetting_CustomSkinAnim)!=0)); + m_checkboxVSync.init(L"VSync",eControl_VSync,(app.GetGameSettings(m_iPad,eGameSetting_VSync)!=0)); + m_checkboxExclusiveFullscreen.init(L"Fullscreen",eControl_ExclusiveFullscreen,(app.GetGameSettings(m_iPad,eGameSetting_ExclusiveFullscreen)!=0)); + - WCHAR TempString[256]; swprintf(TempString, 256, L"Render Distance: %d",app.GetGameSettings(m_iPad,eGameSetting_RenderDistance)); @@ -82,9 +89,15 @@ UIScene_SettingsGraphicsMenu::UIScene_SettingsGraphicsMenu(int iPad, void *initD doHorizontalResizeCheck(); +#ifndef _WINDOWS64 + // VSync and Exclusive Fullscreen are only available on PC + removeControl(&m_checkboxVSync, true); + removeControl(&m_checkboxExclusiveFullscreen, true); +#endif + const bool bInGame=(Minecraft::GetInstance()->level!=nullptr); const bool bIsPrimaryPad=(ProfileManager.GetPrimaryPad()==m_iPad); - // if we're not in the game, we need to use basescene 0 + // if we're not in the game, we need to use basescene 0 if(bInGame) { // If the game has started, then you need to be the host to change the in-game gamertags @@ -165,6 +178,12 @@ void UIScene_SettingsGraphicsMenu::handleInput(int iPad, int key, bool repeat, b app.SetGameSettings(m_iPad,eGameSetting_Clouds,m_checkboxClouds.IsChecked()?1:0); app.SetGameSettings(m_iPad,eGameSetting_BedrockFog,m_checkboxBedrockFog.IsChecked()?1:0); app.SetGameSettings(m_iPad,eGameSetting_CustomSkinAnim,m_checkboxCustomSkinAnim.IsChecked()?1:0); + app.SetGameSettings(m_iPad,eGameSetting_VSync,m_checkboxVSync.IsChecked()?1:0); + app.SetGameSettings(m_iPad,eGameSetting_ExclusiveFullscreen,m_checkboxExclusiveFullscreen.IsChecked()?1:0); +#ifdef _WINDOWS64 + g_bVSync = m_checkboxVSync.IsChecked(); + SetExclusiveFullscreen(m_checkboxExclusiveFullscreen.IsChecked()); +#endif navigateBack(); handled = true; diff --git a/Minecraft.Client/Common/UI/UIScene_SettingsGraphicsMenu.h b/Minecraft.Client/Common/UI/UIScene_SettingsGraphicsMenu.h index 99022c83..ef150f39 100644 --- a/Minecraft.Client/Common/UI/UIScene_SettingsGraphicsMenu.h +++ b/Minecraft.Client/Common/UI/UIScene_SettingsGraphicsMenu.h @@ -12,18 +12,22 @@ private: eControl_Clouds, eControl_BedrockFog, eControl_CustomSkinAnim, + eControl_VSync, + eControl_ExclusiveFullscreen, eControl_RenderDistance, eControl_Gamma, eControl_FOV, eControl_InterfaceOpacity }; - UIControl_CheckBox m_checkboxClouds, m_checkboxBedrockFog, m_checkboxCustomSkinAnim; // Checkboxes + UIControl_CheckBox m_checkboxClouds, m_checkboxBedrockFog, m_checkboxCustomSkinAnim, m_checkboxVSync, m_checkboxExclusiveFullscreen; // Checkboxes UIControl_Slider m_sliderRenderDistance, m_sliderGamma, m_sliderFOV, m_sliderInterfaceOpacity; // Sliders UI_BEGIN_MAP_ELEMENTS_AND_NAMES(UIScene) UI_MAP_ELEMENT( m_checkboxClouds, "Clouds") UI_MAP_ELEMENT( m_checkboxBedrockFog, "BedrockFog") UI_MAP_ELEMENT( m_checkboxCustomSkinAnim, "CustomSkinAnim") + UI_MAP_ELEMENT( m_checkboxVSync, "VSync") + UI_MAP_ELEMENT( m_checkboxExclusiveFullscreen, "ExclusiveFullscreen") UI_MAP_ELEMENT( m_sliderRenderDistance, "RenderDistance") UI_MAP_ELEMENT( m_sliderGamma, "Gamma") UI_MAP_ELEMENT(m_sliderFOV, "FOV") diff --git a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp index e7c7a398..8e112d81 100644 --- a/Minecraft.Client/Windows64/Windows64_Minecraft.cpp +++ b/Minecraft.Client/Windows64/Windows64_Minecraft.cpp @@ -467,6 +467,57 @@ D3D_FEATURE_LEVEL g_featureLevel = D3D_FEATURE_LEVEL_11_0; ID3D11Device* g_pd3dDevice = nullptr; ID3D11DeviceContext* g_pImmediateContext = nullptr; IDXGISwapChain* g_pSwapChain = nullptr; +bool g_bVSync = false; +static bool g_bTearingSupported = false; +static bool g_bPendingExclusiveFullscreen = false; +static bool g_bPendingExclusiveFullscreenValue = false; + +// COM proxy for IDXGISwapChain — delegates all calls to the real swap chain, +// but overrides Present() to set SyncInterval=1 when VSync is enabled. +// Avoids vtable patching, which conflicts with the D3D11 debug layer. +static class SwapChainVSyncProxy : public IDXGISwapChain +{ +public: + void SetTarget(IDXGISwapChain* pReal) { m_pReal = pReal; } + + // IUnknown + HRESULT STDMETHODCALLTYPE QueryInterface(REFIID riid, void** ppvObject) override { return m_pReal->QueryInterface(riid, ppvObject); } + ULONG STDMETHODCALLTYPE AddRef() override { return m_pReal->AddRef(); } + ULONG STDMETHODCALLTYPE Release() override { return m_pReal->Release(); } + + // IDXGIObject + HRESULT STDMETHODCALLTYPE SetPrivateData(REFGUID Name, UINT DataSize, const void* pData) override { return m_pReal->SetPrivateData(Name, DataSize, pData); } + HRESULT STDMETHODCALLTYPE SetPrivateDataInterface(REFGUID Name, const IUnknown* pUnknown) override { return m_pReal->SetPrivateDataInterface(Name, pUnknown); } + HRESULT STDMETHODCALLTYPE GetPrivateData(REFGUID Name, UINT* pDataSize, void* pData) override { return m_pReal->GetPrivateData(Name, pDataSize, pData); } + HRESULT STDMETHODCALLTYPE GetParent(REFIID riid, void** ppParent) override { return m_pReal->GetParent(riid, ppParent); } + + // IDXGIDeviceSubObject + HRESULT STDMETHODCALLTYPE GetDevice(REFIID riid, void** ppDevice) override { return m_pReal->GetDevice(riid, ppDevice); } + + // IDXGISwapChain + HRESULT STDMETHODCALLTYPE Present(UINT SyncInterval, UINT Flags) override + { + if (g_bVSync) + return m_pReal->Present(1, Flags); + // DXGI_PRESENT_ALLOW_TEARING is only valid in windowed mode + if (g_bTearingSupported) + Flags |= DXGI_PRESENT_ALLOW_TEARING; + return m_pReal->Present(0, Flags); + } + HRESULT STDMETHODCALLTYPE GetBuffer(UINT Buffer, REFIID riid, void** ppSurface) override { return m_pReal->GetBuffer(Buffer, riid, ppSurface); } + HRESULT STDMETHODCALLTYPE SetFullscreenState(BOOL Fullscreen, IDXGIOutput* pTarget) override { return m_pReal->SetFullscreenState(Fullscreen, pTarget); } + HRESULT STDMETHODCALLTYPE GetFullscreenState(BOOL* pFullscreen, IDXGIOutput** ppTarget) override { return m_pReal->GetFullscreenState(pFullscreen, ppTarget); } + HRESULT STDMETHODCALLTYPE GetDesc(DXGI_SWAP_CHAIN_DESC* pDesc) override { return m_pReal->GetDesc(pDesc); } + HRESULT STDMETHODCALLTYPE ResizeBuffers(UINT BufferCount, UINT Width, UINT Height, DXGI_FORMAT NewFormat, UINT SwapChainFlags) override { return m_pReal->ResizeBuffers(BufferCount, Width, Height, NewFormat, SwapChainFlags); } + HRESULT STDMETHODCALLTYPE ResizeTarget(const DXGI_MODE_DESC* pNewTargetParameters) override { return m_pReal->ResizeTarget(pNewTargetParameters); } + HRESULT STDMETHODCALLTYPE GetContainingOutput(IDXGIOutput** ppOutput) override { return m_pReal->GetContainingOutput(ppOutput); } + HRESULT STDMETHODCALLTYPE GetFrameStatistics(DXGI_FRAME_STATISTICS* pStats) override { return m_pReal->GetFrameStatistics(pStats); } + HRESULT STDMETHODCALLTYPE GetLastPresentCount(UINT* pLastPresentCount) override { return m_pReal->GetLastPresentCount(pLastPresentCount); } + +private: + IDXGISwapChain* m_pReal = nullptr; +} g_swapChainProxy; + ID3D11RenderTargetView* g_pRenderTargetView = nullptr; ID3D11DepthStencilView* g_pDepthStencilView = nullptr; ID3D11Texture2D* g_pDepthStencilBuffer = nullptr; @@ -817,19 +868,33 @@ HRESULT InitDevice() }; UINT numFeatureLevels = ARRAYSIZE( featureLevels ); + // Check tearing support before device/swap chain creation + { + IDXGIFactory5* factory5 = nullptr; + if (SUCCEEDED(CreateDXGIFactory1(__uuidof(IDXGIFactory5), (void**)&factory5))) + { + BOOL allowTearing = FALSE; + if (SUCCEEDED(factory5->CheckFeatureSupport(DXGI_FEATURE_PRESENT_ALLOW_TEARING, &allowTearing, sizeof(allowTearing)))) + g_bTearingSupported = (allowTearing == TRUE); + factory5->Release(); + } + } + DXGI_SWAP_CHAIN_DESC sd; ZeroMemory( &sd, sizeof( sd ) ); - sd.BufferCount = 1; + sd.BufferCount = 2; sd.BufferDesc.Width = width; sd.BufferDesc.Height = height; sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; - sd.BufferDesc.RefreshRate.Numerator = 60; - sd.BufferDesc.RefreshRate.Denominator = 1; - sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT | DXGI_USAGE_SHADER_INPUT; + sd.BufferDesc.RefreshRate.Numerator = 0; + sd.BufferDesc.RefreshRate.Denominator = 0; + sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; sd.OutputWindow = g_hWnd; sd.SampleDesc.Count = 1; sd.SampleDesc.Quality = 0; sd.Windowed = TRUE; + sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; + sd.Flags = g_bTearingSupported ? DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING : 0; for( UINT driverTypeIndex = 0; driverTypeIndex < numDriverTypes; driverTypeIndex++ ) { @@ -890,7 +955,8 @@ HRESULT InitDevice() vp.TopLeftY = 0; g_pImmediateContext->RSSetViewports( 1, &vp ); - RenderManager.Initialise(g_pd3dDevice, g_pSwapChain); + g_swapChainProxy.SetTarget(g_pSwapChain); + RenderManager.Initialise(g_pd3dDevice, &g_swapChainProxy); PostProcesser::GetInstance().Init(); @@ -906,7 +972,7 @@ void Render() const float ClearColor[4] = { 0.0f, 0.125f, 0.3f, 1.0f }; //red,green,blue,alpha g_pImmediateContext->ClearRenderTargetView( g_pRenderTargetView, ClearColor ); - g_pSwapChain->Present( 0, 0 ); + g_pSwapChain->Present(0, g_bTearingSupported ? DXGI_PRESENT_ALLOW_TEARING : 0); } //-------------------------------------------------------------------------------------- @@ -981,59 +1047,61 @@ static bool ResizeD3D(int newW, int newH) gdraw_D3D11_PreReset(); - // Get IDXGIFactory from the existing device BEFORE destroying the old swap chain. - // If anything fails before we have a new swap chain, we abort without destroying - // the old one — leaving the Renderer in a valid (old-size) state. IDXGISwapChain* pOldSwapChain = g_pSwapChain; bool success = false; HRESULT hr; - IDXGIDevice* dxgiDevice = NULL; - IDXGIAdapter* dxgiAdapter = NULL; - IDXGIFactory* dxgiFactory = NULL; - hr = g_pd3dDevice->QueryInterface(__uuidof(IDXGIDevice), (void**)&dxgiDevice); - if (FAILED(hr)) goto postReset; - hr = dxgiDevice->GetParent(__uuidof(IDXGIAdapter), (void**)&dxgiAdapter); - if (FAILED(hr)) { dxgiDevice->Release(); goto postReset; } - hr = dxgiAdapter->GetParent(__uuidof(IDXGIFactory), (void**)&dxgiFactory); - dxgiAdapter->Release(); - dxgiDevice->Release(); - if (FAILED(hr)) goto postReset; - - // Create new swap chain at backbuffer size + // Create a brand-new swap chain instead of ResizeBuffers. + // ResizeBuffers requires ALL backbuffer refs released, but the closed-source + // Renderer holds hidden refs we can't track — causing DXGI_ERROR_INVALID_CALL + // and leaving the Renderer with NULL views (black screen). + // Creating a new swap chain orphans the old backbuffer (tiny leak) but avoids + // the need to release every hidden reference. { + IDXGIDevice* dxgiDevice = NULL; + IDXGIAdapter* dxgiAdapter = NULL; + IDXGIFactory* dxgiFactory = NULL; + hr = g_pd3dDevice->QueryInterface(__uuidof(IDXGIDevice), (void**)&dxgiDevice); + if (FAILED(hr)) goto postReset; + hr = dxgiDevice->GetParent(__uuidof(IDXGIAdapter), (void**)&dxgiAdapter); + dxgiDevice->Release(); + if (FAILED(hr)) goto postReset; + hr = dxgiAdapter->GetParent(__uuidof(IDXGIFactory), (void**)&dxgiFactory); + dxgiAdapter->Release(); + if (FAILED(hr)) goto postReset; + DXGI_SWAP_CHAIN_DESC sd = {}; - sd.BufferCount = 1; + sd.BufferCount = 2; sd.BufferDesc.Width = bbW; sd.BufferDesc.Height = bbH; sd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM; - sd.BufferDesc.RefreshRate.Numerator = 60; - sd.BufferDesc.RefreshRate.Denominator = 1; - sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT | DXGI_USAGE_SHADER_INPUT; + sd.BufferDesc.RefreshRate.Numerator = 0; + sd.BufferDesc.RefreshRate.Denominator = 0; + sd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT; sd.OutputWindow = g_hWnd; sd.SampleDesc.Count = 1; sd.SampleDesc.Quality = 0; sd.Windowed = TRUE; + sd.SwapEffect = DXGI_SWAP_EFFECT_FLIP_DISCARD; + sd.Flags = g_bTearingSupported ? DXGI_SWAP_CHAIN_FLAG_ALLOW_TEARING : 0; IDXGISwapChain* pNewSwapChain = NULL; hr = dxgiFactory->CreateSwapChain(g_pd3dDevice, &sd, &pNewSwapChain); dxgiFactory->Release(); - if (FAILED(hr) || pNewSwapChain == NULL) + if (FAILED(hr)) { - app.DebugPrintf("[RESIZE] CreateSwapChain FAILED hr=0x%08X — keeping old swap chain\n", (unsigned)hr); + app.DebugPrintf("[RESIZE] CreateSwapChain FAILED hr=0x%08X\n", (unsigned)hr); goto postReset; } - // New swap chain created successfully — NOW destroy the old one. - // The Renderer's internal RTV/SRV still reference the old backbuffer — - // those COM objects become orphaned (tiny leak, harmless). We DON'T - // release them because unknown code may also hold refs. + // Destroy old, install new pOldSwapChain->Release(); g_pSwapChain = pNewSwapChain; + g_swapChainProxy.SetTarget(g_pSwapChain); } - // Patch Renderer's swap chain pointer - *ppRM_SC = g_pSwapChain; + // Patch Renderer's swap chain pointer (use proxy so VSync override stays active) + *ppRM_SC = &g_swapChainProxy; // Create render target views from new backbuffer { @@ -1218,6 +1286,29 @@ void ToggleFullscreen() g_KBMInput.SetWindowFocused(true); } +// Called from UI thread — defers the actual transition to the main game loop +void SetExclusiveFullscreen(bool enabled) +{ + if (enabled == g_isFullscreen) + return; + g_bPendingExclusiveFullscreen = true; + g_bPendingExclusiveFullscreenValue = enabled; +} + +// Uses borderless fullscreen (ToggleFullscreen) rather than DXGI SetFullscreenState. +// With DXGI_SWAP_EFFECT_FLIP_DISCARD + DXGI_PRESENT_ALLOW_TEARING, borderless +// fullscreen gets the same direct-flip path as exclusive fullscreen on Windows 10+ — +// identical latency and uncapped FPS. True DXGI exclusive fullscreen is blocked by +// the 4J Renderer holding hidden backbuffer references that prevent ResizeBuffers. +static void ApplyExclusiveFullscreen(bool enabled) +{ + // Toggle into/out of borderless fullscreen if state doesn't match + if (enabled && !g_isFullscreen) + ToggleFullscreen(); + else if (!enabled && g_isFullscreen) + ToggleFullscreen(); +} + //-------------------------------------------------------------------------------------- // Clean up the objects we've created //-------------------------------------------------------------------------------------- @@ -1801,6 +1892,13 @@ int APIENTRY _tWinMain(_In_ HINSTANCE hInstance, ToggleFullscreen(); } + // Apply deferred exclusive fullscreen toggle + if (g_bPendingExclusiveFullscreen) + { + g_bPendingExclusiveFullscreen = false; + ApplyExclusiveFullscreen(g_bPendingExclusiveFullscreenValue); + } + // TAB opens game info menu. - Vvis :3 - Updated by detectiveren if (g_KBMInput.IsKeyPressed(KeyboardMouseInput::KEY_HOST_SETTINGS) && !ui.GetMenuDisplayed(0)) { diff --git a/Minecraft.Client/stdafx.h b/Minecraft.Client/stdafx.h index d0404009..30f9a20f 100644 --- a/Minecraft.Client/stdafx.h +++ b/Minecraft.Client/stdafx.h @@ -31,6 +31,7 @@ // TODO: reference additional headers your program requires here #include #include +#include using namespace DirectX; #define HRESULT_SUCCEEDED(hr) (((HRESULT)(hr)) >= 0) diff --git a/Minecraft.Server/CMakeLists.txt b/Minecraft.Server/CMakeLists.txt index 52e5826e..a384f7b8 100644 --- a/Minecraft.Server/CMakeLists.txt +++ b/Minecraft.Server/CMakeLists.txt @@ -38,6 +38,7 @@ set_target_properties(Minecraft.Server PROPERTIES target_link_libraries(Minecraft.Server PRIVATE Minecraft.World d3d11 + dxgi d3dcompiler XInput9_1_0 wsock32 diff --git a/Minecraft.World/Level.cpp b/Minecraft.World/Level.cpp index 237a7b48..3e2c150c 100644 --- a/Minecraft.World/Level.cpp +++ b/Minecraft.World/Level.cpp @@ -932,7 +932,6 @@ bool Level::setTileAndData(int x, int y, int z, int tile, int data, int updateFl int old = c->getTile(x & 15, y, z & 15); int olddata = c->getData( x & 15, y, z & 15); #endif - int prevTile = c->getTile(x & 15, y, z & 15); result = c->setTileAndData(x & 15, y, z & 15, tile, data); if( updateFlags != Tile::UPDATE_INVISIBLE_NO_LIGHT) { @@ -940,11 +939,7 @@ bool Level::setTileAndData(int x, int y, int z, int tile, int data, int updateFl PIXBeginNamedEvent(0,"Checking light %d %d %d",x,y,z); PIXBeginNamedEvent(0,"was %d, %d now %d, %d",old,olddata,tile,data); #endif - if (Tile::lightBlock[tile & 0xff] != Tile::lightBlock[prevTile & 0xff] || - Tile::lightEmission[tile & 0xff] != Tile::lightEmission[prevTile & 0xff]) - { - checkLight(x, y, z); - } + checkLight(x, y, z); PIXEndNamedEvent(); PIXEndNamedEvent(); } diff --git a/Minecraft.World/LevelChunk.cpp b/Minecraft.World/LevelChunk.cpp index a0749a2f..cc3d9fb0 100644 --- a/Minecraft.World/LevelChunk.cpp +++ b/Minecraft.World/LevelChunk.cpp @@ -887,16 +887,11 @@ void LevelChunk::recalcHeight(int x, int yStart, int z) if (!level->dimension->hasCeiling) { PIXBeginNamedEvent(0,"Light gaps"); - // Flag columns for gap rechecking — processed by recheckGaps() on next tick - auto flagGap = [&](int wx, int wz) { - LevelChunk *c = level->getChunkAt(wx, wz); - if (c && !c->isEmpty()) c->lightGaps(wx & 15, wz & 15); - }; - flagGap(xOffs - 1, zOffs); - flagGap(xOffs + 1, zOffs); - flagGap(xOffs, zOffs - 1); - flagGap(xOffs, zOffs + 1); - flagGap(xOffs, zOffs); + lightGap(xOffs - 1, zOffs, y1, y2); + lightGap(xOffs + 1, zOffs, y1, y2); + lightGap(xOffs, zOffs - 1, y1, y2); + lightGap(xOffs, zOffs + 1, y1, y2); + lightGap(xOffs, zOffs, y1, y2); PIXEndNamedEvent(); } diff --git a/tools/AddExclusiveFullscreenCheckbox.class b/tools/AddExclusiveFullscreenCheckbox.class new file mode 100644 index 00000000..09973d12 Binary files /dev/null and b/tools/AddExclusiveFullscreenCheckbox.class differ diff --git a/tools/AddExclusiveFullscreenCheckbox.java b/tools/AddExclusiveFullscreenCheckbox.java new file mode 100644 index 00000000..cbfbf8d5 --- /dev/null +++ b/tools/AddExclusiveFullscreenCheckbox.java @@ -0,0 +1,158 @@ +import com.jpexs.decompiler.flash.SWF; +import com.jpexs.decompiler.flash.tags.*; +import com.jpexs.decompiler.flash.tags.base.*; +import com.jpexs.decompiler.flash.types.*; +import java.io.*; +import java.util.*; + +/** + * Adds an "ExclusiveFullscreen" checkbox to SettingsGraphicsMenu SWF files. + * Clones the existing VSync checkbox, renames it "ExclusiveFullscreen", + * positions it below VSync, and shifts sliders down. + */ +public class AddExclusiveFullscreenCheckbox { + public static void main(String[] args) throws Exception { + if (args.length < 1) { + System.out.println("Usage: AddExclusiveFullscreenCheckbox [output_file]"); + System.exit(1); + } + + String inputPath = args[0]; + String outputPath = args.length > 1 ? args[1] : inputPath; + + SWF swf = new SWF(new FileInputStream(inputPath), false); + + // Check if ExclusiveFullscreen already exists + for (Tag tag : swf.getTags()) { + if (tag instanceof PlaceObject3Tag) { + PlaceObject3Tag po = (PlaceObject3Tag) tag; + if ("ExclusiveFullscreen".equals(po.name)) { + System.out.println("ExclusiveFullscreen checkbox already exists in " + inputPath + ", skipping."); + return; + } + } + } + + // Find the VSync checkbox tag and all slider tags + PlaceObject3Tag vsyncTag = null; + PlaceObject3Tag customSkinAnimTag = null; + List sliderTags = new ArrayList<>(); + int maxDepth = 0; + + for (Tag tag : swf.getTags()) { + if (tag instanceof PlaceObject3Tag) { + PlaceObject3Tag po = (PlaceObject3Tag) tag; + if ("VSync".equals(po.name)) { + vsyncTag = po; + } + if ("CustomSkinAnim".equals(po.name)) { + customSkinAnimTag = po; + } + if (po.name != null && (po.name.equals("RenderDistance") || po.name.equals("Gamma") + || po.name.equals("FOV") || po.name.equals("InterfaceOpacity"))) { + sliderTags.add(po); + } + if (po.depth > maxDepth) maxDepth = po.depth; + } + if (tag instanceof PlaceObject2Tag) { + PlaceObject2Tag po = (PlaceObject2Tag) tag; + if (po.depth > maxDepth) maxDepth = po.depth; + } + } + + if (vsyncTag == null) { + System.err.println("ERROR: Could not find VSync checkbox in " + inputPath); + System.exit(1); + } + + // Calculate checkbox Y-spacing from the gap between CustomSkinAnim and VSync + int checkboxSpacing; + if (customSkinAnimTag != null) { + checkboxSpacing = vsyncTag.matrix.translateY - customSkinAnimTag.matrix.translateY; + } else { + checkboxSpacing = vsyncTag.matrix.translateY > 8000 ? 1080 : 720; + } + + System.out.println("File: " + inputPath); + System.out.println(" VSync Y: " + vsyncTag.matrix.translateY); + System.out.println(" Checkbox spacing: " + checkboxSpacing); + + // Create the ExclusiveFullscreen PlaceObject3Tag by copying fields from VSync + PlaceObject3Tag efTag = new PlaceObject3Tag(swf); + efTag.placeFlagHasClassName = vsyncTag.placeFlagHasClassName; + efTag.placeFlagHasName = true; + efTag.placeFlagHasMatrix = true; + efTag.placeFlagHasImage = vsyncTag.placeFlagHasImage; + efTag.placeFlagHasCharacter = vsyncTag.placeFlagHasCharacter; + efTag.placeFlagMove = vsyncTag.placeFlagMove; + efTag.className = vsyncTag.className; + efTag.name = "ExclusiveFullscreen"; + efTag.depth = maxDepth + 1; + efTag.characterId = vsyncTag.characterId; + + // Copy and offset the matrix + MATRIX m = new MATRIX(vsyncTag.matrix); + m.translateY += checkboxSpacing; + efTag.matrix = m; + + efTag.setModified(true); + + System.out.println(" ExclusiveFullscreen Y: " + m.translateY); + System.out.println(" ExclusiveFullscreen depth: " + efTag.depth); + + // Shift all sliders down by checkboxSpacing + for (PlaceObject3Tag slider : sliderTags) { + System.out.println(" Shifting " + slider.name + " from Y=" + slider.matrix.translateY + + " to Y=" + (slider.matrix.translateY + checkboxSpacing)); + slider.matrix.translateY += checkboxSpacing; + slider.setModified(true); + } + + // Expand the background panel height + for (Tag tag : swf.getTags()) { + if (tag instanceof PlaceObject2Tag) { + PlaceObject2Tag po = (PlaceObject2Tag) tag; + if ("BackgroundPanel".equals(po.name)) { + int charId = po.characterId; + CharacterTag ct = swf.getCharacter(charId); + if (ct instanceof DefineSpriteTag) { + DefineSpriteTag sprite = (DefineSpriteTag) ct; + for (Tag sub : sprite.getTags()) { + if (sub instanceof PlaceObject3Tag) { + PlaceObject3Tag gridTag = (PlaceObject3Tag) sub; + float oldScaleY = gridTag.matrix.scaleY; + gridTag.matrix.scaleY += (float) checkboxSpacing / 640.0f; + gridTag.setModified(true); + System.out.println(" Background panel scaleY: " + oldScaleY + " -> " + gridTag.matrix.scaleY); + } + } + } + } + } + } + + // Insert ExclusiveFullscreen tag right after VSync + ArrayList tagList = swf.getTags().toArrayList(); + int insertIdx = -1; + for (int i = 0; i < tagList.size(); i++) { + if (tagList.get(i) == vsyncTag) { + insertIdx = i + 1; + break; + } + } + + if (insertIdx >= 0) { + swf.addTag(insertIdx, efTag); + System.out.println(" Inserted ExclusiveFullscreen tag at index " + insertIdx); + } else { + System.err.println("ERROR: Could not find insertion point"); + System.exit(1); + } + + // Save + try (FileOutputStream fos = new FileOutputStream(outputPath)) { + swf.saveTo(fos); + } + System.out.println(" Saved to: " + outputPath); + } +} diff --git a/tools/AddVSyncCheckbox.class b/tools/AddVSyncCheckbox.class new file mode 100644 index 00000000..ce0de93e Binary files /dev/null and b/tools/AddVSyncCheckbox.class differ diff --git a/tools/AddVSyncCheckbox.java b/tools/AddVSyncCheckbox.java new file mode 100644 index 00000000..618c6389 --- /dev/null +++ b/tools/AddVSyncCheckbox.java @@ -0,0 +1,160 @@ +import com.jpexs.decompiler.flash.SWF; +import com.jpexs.decompiler.flash.tags.*; +import com.jpexs.decompiler.flash.tags.base.*; +import com.jpexs.decompiler.flash.types.*; +import java.io.*; +import java.util.*; + +/** + * Adds a "VSync" checkbox to SettingsGraphicsMenu SWF files. + * Clones the existing CustomSkinAnim checkbox, renames it "VSync", + * positions it below CustomSkinAnim, and shifts sliders down. + */ +public class AddVSyncCheckbox { + public static void main(String[] args) throws Exception { + if (args.length < 1) { + System.out.println("Usage: AddVSyncCheckbox [output_file]"); + System.exit(1); + } + + String inputPath = args[0]; + String outputPath = args.length > 1 ? args[1] : inputPath; + + SWF swf = new SWF(new FileInputStream(inputPath), false); + + // Check if VSync already exists + for (Tag tag : swf.getTags()) { + if (tag instanceof PlaceObject3Tag) { + PlaceObject3Tag po = (PlaceObject3Tag) tag; + if ("VSync".equals(po.name)) { + System.out.println("VSync checkbox already exists in " + inputPath + ", skipping."); + return; + } + } + } + + // Find the CustomSkinAnim checkbox tag and all slider tags + PlaceObject3Tag customSkinAnimTag = null; + PlaceObject3Tag bedrockFogTag = null; + List sliderTags = new ArrayList<>(); + int maxDepth = 0; + + for (Tag tag : swf.getTags()) { + if (tag instanceof PlaceObject3Tag) { + PlaceObject3Tag po = (PlaceObject3Tag) tag; + if ("CustomSkinAnim".equals(po.name)) { + customSkinAnimTag = po; + } + if ("BedrockFog".equals(po.name)) { + bedrockFogTag = po; + } + if (po.name != null && (po.name.equals("RenderDistance") || po.name.equals("Gamma") + || po.name.equals("FOV") || po.name.equals("InterfaceOpacity"))) { + sliderTags.add(po); + } + if (po.depth > maxDepth) maxDepth = po.depth; + } + if (tag instanceof PlaceObject2Tag) { + PlaceObject2Tag po = (PlaceObject2Tag) tag; + if (po.depth > maxDepth) maxDepth = po.depth; + } + } + + if (customSkinAnimTag == null) { + System.err.println("ERROR: Could not find CustomSkinAnim checkbox in " + inputPath); + System.exit(1); + } + + // Calculate checkbox Y-spacing + int checkboxSpacing; + if (bedrockFogTag != null) { + checkboxSpacing = customSkinAnimTag.matrix.translateY - bedrockFogTag.matrix.translateY; + } else { + checkboxSpacing = customSkinAnimTag.matrix.translateY > 8000 ? 1080 : 720; + } + + System.out.println("File: " + inputPath); + System.out.println(" CustomSkinAnim Y: " + customSkinAnimTag.matrix.translateY); + System.out.println(" Checkbox spacing: " + checkboxSpacing); + + // Create the VSync PlaceObject3Tag by copying fields from CustomSkinAnim + PlaceObject3Tag vsyncTag = new PlaceObject3Tag(swf); + vsyncTag.placeFlagHasClassName = customSkinAnimTag.placeFlagHasClassName; + vsyncTag.placeFlagHasName = true; + vsyncTag.placeFlagHasMatrix = true; + vsyncTag.placeFlagHasImage = customSkinAnimTag.placeFlagHasImage; + vsyncTag.placeFlagHasCharacter = customSkinAnimTag.placeFlagHasCharacter; + vsyncTag.placeFlagMove = customSkinAnimTag.placeFlagMove; + vsyncTag.className = customSkinAnimTag.className; + vsyncTag.name = "VSync"; + vsyncTag.depth = maxDepth + 1; + vsyncTag.characterId = customSkinAnimTag.characterId; + + // Copy and offset the matrix + MATRIX m = new MATRIX(customSkinAnimTag.matrix); + m.translateY += checkboxSpacing; + vsyncTag.matrix = m; + + vsyncTag.setModified(true); + + System.out.println(" VSync Y: " + m.translateY); + System.out.println(" VSync depth: " + vsyncTag.depth); + System.out.println(" VSync className: " + vsyncTag.className); + + // Shift all sliders down by checkboxSpacing + for (PlaceObject3Tag slider : sliderTags) { + System.out.println(" Shifting " + slider.name + " from Y=" + slider.matrix.translateY + + " to Y=" + (slider.matrix.translateY + checkboxSpacing)); + slider.matrix.translateY += checkboxSpacing; + slider.setModified(true); + } + + // Expand the background panel height + for (Tag tag : swf.getTags()) { + if (tag instanceof PlaceObject2Tag) { + PlaceObject2Tag po = (PlaceObject2Tag) tag; + if ("BackgroundPanel".equals(po.name)) { + int charId = po.characterId; + CharacterTag ct = swf.getCharacter(charId); + if (ct instanceof DefineSpriteTag) { + DefineSpriteTag sprite = (DefineSpriteTag) ct; + for (Tag sub : sprite.getTags()) { + if (sub instanceof PlaceObject3Tag) { + PlaceObject3Tag gridTag = (PlaceObject3Tag) sub; + float oldScaleY = gridTag.matrix.scaleY; + // The 9-grid base tile is 640 twips (32px * 20 twips/px) + gridTag.matrix.scaleY += (float) checkboxSpacing / 640.0f; + gridTag.setModified(true); + System.out.println(" Background panel scaleY: " + oldScaleY + " -> " + gridTag.matrix.scaleY); + } + } + } + } + } + } + + // Insert VSync tag right after CustomSkinAnim + ArrayList tagList = swf.getTags().toArrayList(); + int insertIdx = -1; + for (int i = 0; i < tagList.size(); i++) { + if (tagList.get(i) == customSkinAnimTag) { + insertIdx = i + 1; + break; + } + } + + if (insertIdx >= 0) { + swf.addTag(insertIdx, vsyncTag); + System.out.println(" Inserted VSync tag at index " + insertIdx); + } else { + System.err.println("ERROR: Could not find insertion point"); + System.exit(1); + } + + // Save + try (FileOutputStream fos = new FileOutputStream(outputPath)) { + swf.saveTo(fos); + } + System.out.println(" Saved to: " + outputPath); + } +} diff --git a/tools/DumpSwf.class b/tools/DumpSwf.class new file mode 100644 index 00000000..4125d7e6 Binary files /dev/null and b/tools/DumpSwf.class differ diff --git a/tools/DumpSwf.java b/tools/DumpSwf.java new file mode 100644 index 00000000..2e719a98 --- /dev/null +++ b/tools/DumpSwf.java @@ -0,0 +1,40 @@ +import com.jpexs.decompiler.flash.SWF; +import com.jpexs.decompiler.flash.tags.*; +import com.jpexs.decompiler.flash.tags.base.*; +import com.jpexs.decompiler.flash.types.*; +import java.io.*; +import java.util.*; + +public class DumpSwf { + public static void main(String[] args) throws Exception { + String path = args[0]; + SWF swf = new SWF(new FileInputStream(path), false); + + System.out.println("=== Top-level Tags ==="); + int idx = 0; + for (Tag tag : swf.getTags()) { + System.out.println("[" + idx + "] " + tag.getClass().getSimpleName() + " - " + tag); + dumpPlaceObject(tag, " "); + + if (tag instanceof DefineSpriteTag) { + DefineSpriteTag sprite = (DefineSpriteTag) tag; + System.out.println(" spriteId=" + sprite.spriteId + " frames=" + sprite.frameCount); + int subIdx = 0; + for (Tag sub : sprite.getTags()) { + System.out.println(" [" + subIdx + "] " + sub.getClass().getSimpleName() + " - " + sub); + dumpPlaceObject(sub, " "); + subIdx++; + } + } + idx++; + } + } + + static void dumpPlaceObject(Tag tag, String indent) { + if (tag instanceof PlaceObjectTypeTag) { + PlaceObjectTypeTag po = (PlaceObjectTypeTag) tag; + System.out.println(indent + "depth=" + po.getDepth() + " charId=" + po.getCharacterId() + + " name=" + po.getInstanceName() + " matrix=" + po.getMatrix()); + } + } +} diff --git a/tools/RebuildArc.class b/tools/RebuildArc.class new file mode 100644 index 00000000..5d4c67b7 Binary files /dev/null and b/tools/RebuildArc.class differ diff --git a/tools/RebuildArc.java b/tools/RebuildArc.java new file mode 100644 index 00000000..dc00747b --- /dev/null +++ b/tools/RebuildArc.java @@ -0,0 +1,168 @@ +import java.io.*; +import java.nio.file.*; +import java.util.*; + +/** + * Rebuilds a MediaWindows64.arc archive file by replacing specific SWF files + * with updated versions from disk. + * + * Usage: RebuildArc [file1.swf file2.swf ...] + * + * If no specific files are given, replaces all SWF files found in media_dir. + * The arc format is Java DataOutputStream style: big-endian ints, modified UTF-8 strings. + * + * Archive format: + * int: numberOfFiles + * For each file: + * UTF: filename (prefixed with '*' if compressed) + * int: offset into data section + * int: filesize + * Raw file data follows the header + */ +public class RebuildArc { + public static void main(String[] args) throws Exception { + if (args.length < 2) { + System.out.println("Usage: RebuildArc [file1.swf file2.swf ...]"); + System.exit(1); + } + + String arcPath = args[0]; + String mediaDir = args[1]; + Set replaceFiles = new HashSet<>(); + for (int i = 2; i < args.length; i++) { + replaceFiles.add(args[i]); + } + + // Read the original archive + DataInputStream dis = new DataInputStream(new FileInputStream(arcPath)); + int numberOfFiles = dis.readInt(); + + System.out.println("Archive has " + numberOfFiles + " files"); + + // Read the index + List filenames = new ArrayList<>(); + List offsets = new ArrayList<>(); + List sizes = new ArrayList<>(); + List compressed = new ArrayList<>(); + + for (int i = 0; i < numberOfFiles; i++) { + String name = dis.readUTF(); + int offset = dis.readInt(); + int size = dis.readInt(); + + boolean isCompressed = false; + if (name.startsWith("*")) { + name = name.substring(1); + isCompressed = true; + } + + filenames.add(name); + offsets.add(offset); + sizes.add(size); + compressed.add(isCompressed); + } + + // The header size is the current position - data offsets are relative to file start + // Read the entire file to get the raw data + dis.close(); + byte[] arcData = Files.readAllBytes(Paths.get(arcPath)); + + // Build replacement data map + // For each file, either use the replacement or the original data + List fileData = new ArrayList<>(); + int replacedCount = 0; + + for (int i = 0; i < numberOfFiles; i++) { + String name = filenames.get(i); + String simpleName = name.contains("\\") ? name.substring(name.lastIndexOf('\\') + 1) : name; + + boolean shouldReplace = false; + if (replaceFiles.isEmpty()) { + // Replace all SWFs found in mediaDir + File diskFile = new File(mediaDir, simpleName); + if (diskFile.exists() && simpleName.endsWith(".swf")) { + shouldReplace = true; + } + } else { + shouldReplace = replaceFiles.contains(simpleName); + } + + if (shouldReplace) { + File diskFile = new File(mediaDir, simpleName); + if (diskFile.exists()) { + byte[] newData = Files.readAllBytes(diskFile.toPath()); + fileData.add(newData); + // Replaced files are NOT compressed (our SWFs are uncompressed) + compressed.set(i, false); + sizes.set(i, newData.length); + System.out.println(" Replacing: " + name + " (" + newData.length + " bytes)"); + replacedCount++; + } else { + System.err.println(" WARNING: " + diskFile + " not found, keeping original"); + byte[] original = new byte[sizes.get(i)]; + System.arraycopy(arcData, offsets.get(i), original, 0, sizes.get(i)); + fileData.add(original); + } + } else { + // Keep original data + byte[] original = new byte[sizes.get(i)]; + System.arraycopy(arcData, offsets.get(i), original, 0, sizes.get(i)); + fileData.add(original); + } + } + + if (replacedCount == 0) { + System.out.println("No files were replaced!"); + System.exit(1); + } + + // Calculate new header size to compute data offsets + // Header: 4 bytes (numFiles) + for each file: (2 + UTF8 length + 4 + 4) bytes + ByteArrayOutputStream headerBuf = new ByteArrayOutputStream(); + DataOutputStream headerDos = new DataOutputStream(headerBuf); + headerDos.writeInt(numberOfFiles); + for (int i = 0; i < numberOfFiles; i++) { + String name = filenames.get(i); + if (compressed.get(i)) { + name = "*" + name; + } + headerDos.writeUTF(name); + headerDos.writeInt(0); // placeholder offset + headerDos.writeInt(0); // placeholder size + } + headerDos.flush(); + int headerSize = headerBuf.size(); + + // Now compute real offsets + int currentOffset = headerSize; + List newOffsets = new ArrayList<>(); + for (int i = 0; i < numberOfFiles; i++) { + newOffsets.add(currentOffset); + currentOffset += fileData.get(i).length; + } + + // Write the final archive + String outputPath = arcPath; // overwrite in place + DataOutputStream dos = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(outputPath))); + dos.writeInt(numberOfFiles); + for (int i = 0; i < numberOfFiles; i++) { + String name = filenames.get(i); + if (compressed.get(i)) { + name = "*" + name; + } + dos.writeUTF(name); + dos.writeInt(newOffsets.get(i)); + dos.writeInt(fileData.get(i).length); + } + + // Write file data + for (int i = 0; i < numberOfFiles; i++) { + dos.write(fileData.get(i)); + } + + dos.flush(); + dos.close(); + + System.out.println("Rebuilt archive: " + outputPath + " (" + replacedCount + " files replaced)"); + } +}