Merge branch 'main' into mem-alloc-check

This commit is contained in:
Skyth (Asilkan) 2025-03-28 21:00:14 +03:00 committed by GitHub
commit 00929356b5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
46 changed files with 1058 additions and 252 deletions

10
.gitignore vendored
View file

@ -397,3 +397,13 @@ FodyWeavers.xsd
# JetBrains Rider
*.sln.iml
# IntelliJ IDEs
.idea/
# macOS metadata
*.DS_Store
# CMake Files
**/cmake-build-debug
**/CMakeCache.txt

View file

@ -183,6 +183,8 @@ set(UNLEASHED_RECOMP_USER_CXX_SOURCES
"user/config.cpp"
"user/registry.cpp"
"user/paths.cpp"
"user/persistent_data.cpp"
"user/persistent_storage_manager.cpp"
)
set(UNLEASHED_RECOMP_MOD_CXX_SOURCES
@ -229,7 +231,8 @@ set(UNLEASHED_RECOMP_CXX_SOURCES
"app.cpp"
"exports.cpp"
"main.cpp"
"misc_impl.cpp"
"misc_impl.cpp"
"preload_executable.cpp"
"sdl_listener.cpp"
"stdafx.cpp"
"version.cpp"
@ -310,7 +313,11 @@ endif()
if (UNLEASHED_RECOMP_D3D12)
find_package(directx-headers CONFIG REQUIRED)
find_package(directx12-agility CONFIG REQUIRED)
target_compile_definitions(UnleashedRecomp PRIVATE UNLEASHED_RECOMP_D3D12)
target_compile_definitions(UnleashedRecomp PRIVATE
UNLEASHED_RECOMP_D3D12
D3D12MA_USING_DIRECTX_HEADERS
D3D12MA_OPTIONS16_SUPPORTED
)
endif()
if (CMAKE_SYSTEM_NAME MATCHES "Linux")

View file

@ -80,9 +80,13 @@ namespace SWA
boost::shared_ptr<Hedgehog::Mirage::CRenderScene> m_spRenderScene;
SWA_INSERT_PADDING(0x04);
boost::shared_ptr<CGameParameter> m_spGameParameter;
SWA_INSERT_PADDING(0x78);
SWA_INSERT_PADDING(0x0C);
boost::anonymous_shared_ptr m_spItemParamManager;
SWA_INSERT_PADDING(0x64);
boost::shared_ptr<Hedgehog::Base::CCriticalSection> m_spCriticalSection;
SWA_INSERT_PADDING(0x20);
SWA_INSERT_PADDING(0x14);
bool m_ShowDLCInfo;
SWA_INSERT_PADDING(0x08);
};
// TODO: Hedgehog::Base::TSynchronizedPtr<CApplicationDocument>
@ -111,7 +115,9 @@ namespace SWA
SWA_ASSERT_OFFSETOF(CApplicationDocument::CMember, m_Field10C, 0x10C);
SWA_ASSERT_OFFSETOF(CApplicationDocument::CMember, m_spRenderScene, 0x12C);
SWA_ASSERT_OFFSETOF(CApplicationDocument::CMember, m_spGameParameter, 0x138);
SWA_ASSERT_OFFSETOF(CApplicationDocument::CMember, m_spItemParamManager, 0x14C);
SWA_ASSERT_OFFSETOF(CApplicationDocument::CMember, m_spCriticalSection, 0x1B8);
SWA_ASSERT_OFFSETOF(CApplicationDocument::CMember, m_ShowDLCInfo, 0x1D4);
SWA_ASSERT_SIZEOF(CApplicationDocument::CMember, 0x1E0);
SWA_ASSERT_OFFSETOF(CApplicationDocument, m_pMember, 0x04);

View file

@ -7,11 +7,19 @@ namespace SWA
class CGameParameter // : public Hedgehog::Universe::CMessageActor
{
public:
struct SSaveData;
struct SSaveData
{
SWA_INSERT_PADDING(0x8600);
be<uint32_t> DLCFlags[8];
SWA_INSERT_PADDING(0x15C);
};
struct SStageParameter;
SWA_INSERT_PADDING(0x94);
xpointer<SSaveData> m_pSaveData;
xpointer<SStageParameter> m_pStageParameter;
};
SWA_ASSERT_OFFSETOF(CGameParameter::SSaveData, DLCFlags, 0x8600);
}

View file

@ -10,7 +10,6 @@
void XAudioInitializeSystem();
void XAudioRegisterClient(PPCFunc* callback, uint32_t param);
void XAudioSubmitFrame(void* samples);
void XAudioConfigValueChangedCallback(class IConfigDef* configDef);
uint32_t XAudioRegisterRenderDriverClient(be<uint32_t>* callback, be<uint32_t>* driver);
uint32_t XAudioUnregisterRenderDriverClient(uint32_t driver);

View file

@ -150,18 +150,3 @@ void XAudioSubmitFrame(void* samples)
SDL_QueueAudio(g_audioDevice, &audioFrames, sizeof(audioFrames));
}
}
void XAudioConfigValueChangedCallback(IConfigDef* configDef)
{
if (configDef == &Config::ChannelConfiguration)
{
if (g_audioThread->joinable())
{
g_audioThreadShouldExit = true;
g_audioThread->join();
}
CreateAudioDevice();
CreateAudioThread();
}
}

View file

@ -442,6 +442,8 @@ namespace plume {
return D3D12_HEAP_TYPE_UPLOAD;
case RenderHeapType::READBACK:
return D3D12_HEAP_TYPE_READBACK;
case RenderHeapType::GPU_UPLOAD:
return D3D12_HEAP_TYPE_GPU_UPLOAD;
default:
assert(false && "Unknown heap type.");
return D3D12_HEAP_TYPE_DEFAULT;
@ -2385,7 +2387,7 @@ namespace plume {
range.End = readRange->end;
}
void *outputData;
void *outputData = nullptr;
d3d->Map(subresource, (readRange != nullptr) ? &range : nullptr, &outputData);
return outputData;
}
@ -2629,14 +2631,22 @@ namespace plume {
// D3D12Pool
D3D12Pool::D3D12Pool(D3D12Device *device, const RenderPoolDesc &desc) {
D3D12Pool::D3D12Pool(D3D12Device *device, const RenderPoolDesc &desc, bool gpuUploadHeapFallback) {
assert(device != nullptr);
this->device = device;
this->desc = desc;
D3D12MA::POOL_DESC poolDesc = {};
poolDesc.HeapProperties.Type = toD3D12(desc.heapType);
// When using an UMA architecture without explicit support for GPU Upload heaps, we instead just make a custom heap with the same properties as Upload heaps.
if ((desc.heapType == RenderHeapType::GPU_UPLOAD) && gpuUploadHeapFallback) {
poolDesc.HeapProperties = device->d3d->GetCustomHeapProperties(0, D3D12_HEAP_TYPE_UPLOAD);
}
else {
poolDesc.HeapProperties.Type = toD3D12(desc.heapType);
}
poolDesc.MinBlockCount = desc.minBlockCount;
poolDesc.MaxBlockCount = desc.maxBlockCount;
poolDesc.Flags |= desc.useLinearAlgorithm ? D3D12MA::POOL_FLAG_ALGORITHM_LINEAR : D3D12MA::POOL_FLAG_NONE;
@ -3390,13 +3400,15 @@ namespace plume {
if (SUCCEEDED(res)) {
triangleFanSupportOption = d3d12Options15.TriangleFanSupported;
}
// Check if dynamic depth bias is supported.
// Check if dynamic depth bias and GPU upload heap are supported.
bool dynamicDepthBiasOption = false;
bool gpuUploadHeapOption = false;
D3D12_FEATURE_DATA_D3D12_OPTIONS16 d3d12Options16 = {};
res = deviceOption->CheckFeatureSupport(D3D12_FEATURE_D3D12_OPTIONS16, &d3d12Options16, sizeof(d3d12Options16));
if (SUCCEEDED(res)) {
dynamicDepthBiasOption = d3d12Options16.DynamicDepthBiasSupported;
gpuUploadHeapOption = d3d12Options16.GPUUploadHeapSupported;
}
// Check if the architecture has UMA.
@ -3431,6 +3443,11 @@ namespace plume {
capabilities.triangleFan = triangleFanSupportOption;
capabilities.dynamicDepthBias = dynamicDepthBiasOption;
capabilities.uma = uma;
// Pretend GPU Upload heaps are supported if UMA is supported, as the backend has a workaround using a custom pool for it.
capabilities.gpuUploadHeap = uma || gpuUploadHeapOption;
gpuUploadHeapFallback = uma && !gpuUploadHeapOption;
description.name = deviceName;
description.dedicatedVideoMemory = adapterDesc.DedicatedVideoMemory;
description.vendor = RenderDeviceVendor(adapterDesc.VendorId);
@ -3528,6 +3545,13 @@ namespace plume {
colorTargetHeapAllocator = std::make_unique<D3D12DescriptorHeapAllocator>(this, TargetDescriptorHeapSize, D3D12_DESCRIPTOR_HEAP_TYPE_RTV);
depthTargetHeapAllocator = std::make_unique<D3D12DescriptorHeapAllocator>(this, TargetDescriptorHeapSize, D3D12_DESCRIPTOR_HEAP_TYPE_DSV);
// Create the custom upload pool that will be used as the fallback when using an UMA architecture without explicit support for GPU Upload heaps.
if (gpuUploadHeapFallback) {
RenderPoolDesc poolDesc;
poolDesc.heapType = RenderHeapType::GPU_UPLOAD;
customUploadPool = std::make_unique<D3D12Pool>(this, poolDesc, true);
}
// Create a command queue only for retrieving the timestamp frequency. Delete it immediately afterwards.
std::unique_ptr<D3D12CommandQueue> timestampCommandQueue = std::make_unique<D3D12CommandQueue>(this, RenderCommandListType::DIRECT);
res = timestampCommandQueue->d3d->GetTimestampFrequency(&timestampFrequency);
@ -3577,7 +3601,12 @@ namespace plume {
}
std::unique_ptr<RenderBuffer> D3D12Device::createBuffer(const RenderBufferDesc &desc) {
return std::make_unique<D3D12Buffer>(this, nullptr, desc);
if ((desc.heapType == RenderHeapType::GPU_UPLOAD) && gpuUploadHeapFallback) {
return std::make_unique<D3D12Buffer>(this, customUploadPool.get(), desc);
}
else {
return std::make_unique<D3D12Buffer>(this, nullptr, desc);
}
}
std::unique_ptr<RenderTexture> D3D12Device::createTexture(const RenderTextureDesc &desc) {
@ -3589,7 +3618,7 @@ namespace plume {
}
std::unique_ptr<RenderPool> D3D12Device::createPool(const RenderPoolDesc &desc) {
return std::make_unique<D3D12Pool>(this, desc);
return std::make_unique<D3D12Pool>(this, desc, gpuUploadHeapFallback);
}
std::unique_ptr<RenderPipelineLayout> D3D12Device::createPipelineLayout(const RenderPipelineLayoutDesc &desc) {

View file

@ -329,7 +329,7 @@ namespace plume {
D3D12Device *device = nullptr;
RenderPoolDesc desc;
D3D12Pool(D3D12Device *device, const RenderPoolDesc &desc);
D3D12Pool(D3D12Device *device, const RenderPoolDesc &desc, bool gpuUploadHeapFallback);
~D3D12Pool() override;
std::unique_ptr<RenderBuffer> createBuffer(const RenderBufferDesc &desc) override;
std::unique_ptr<RenderTexture> createTexture(const RenderTextureDesc &desc) override;
@ -430,9 +430,11 @@ namespace plume {
std::unique_ptr<D3D12DescriptorHeapAllocator> samplerHeapAllocator;
std::unique_ptr<D3D12DescriptorHeapAllocator> colorTargetHeapAllocator;
std::unique_ptr<D3D12DescriptorHeapAllocator> depthTargetHeapAllocator;
std::unique_ptr<D3D12Pool> customUploadPool;
RenderDeviceCapabilities capabilities;
RenderDeviceDescription description;
uint64_t timestampFrequency = 1;
bool gpuUploadHeapFallback = false;
D3D12Device(D3D12Interface *renderInterface, const std::string &preferredDeviceName);
~D3D12Device() override;

View file

@ -351,7 +351,8 @@ namespace plume {
UNKNOWN,
DEFAULT,
UPLOAD,
READBACK
READBACK,
GPU_UPLOAD
};
enum class RenderTextureArrangement {
@ -1807,6 +1808,9 @@ namespace plume {
// UMA.
bool uma = false;
// GPU Upload heap.
bool gpuUploadHeap = false;
};
struct RenderInterfaceCapabilities {

View file

@ -808,6 +808,12 @@ namespace plume {
bufferInfo.usage |= VK_BUFFER_USAGE_TRANSFER_DST_BIT;
createInfo.flags |= VMA_ALLOCATION_CREATE_HOST_ACCESS_RANDOM_BIT;
break;
case RenderHeapType::GPU_UPLOAD:
bufferInfo.usage |= VK_BUFFER_USAGE_TRANSFER_SRC_BIT;
bufferInfo.usage |= VK_BUFFER_USAGE_TRANSFER_DST_BIT;
createInfo.flags |= VMA_ALLOCATION_CREATE_HOST_ACCESS_SEQUENTIAL_WRITE_BIT;
createInfo.requiredFlags |= VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT | VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT;
break;
default:
assert(false && "Unknown heap type.");
break;
@ -833,7 +839,7 @@ namespace plume {
}
if (res != VK_SUCCESS) {
fprintf(stderr, "vkCreateBuffer failed with error code 0x%X.\n", res);
fprintf(stderr, "vmaCreateBuffer failed with error code 0x%X.\n", res);
return;
}
}
@ -3887,6 +3893,15 @@ namespace plume {
VkDeviceSize memoryHeapSize = 0;
const VkPhysicalDeviceMemoryProperties *memoryProps = nullptr;
vmaGetMemoryProperties(allocator, &memoryProps);
constexpr VkMemoryPropertyFlags uploadHeapPropertyFlags = VK_MEMORY_PROPERTY_HOST_VISIBLE_BIT | VK_MEMORY_PROPERTY_DEVICE_LOCAL_BIT;
bool hasHostVisibleDeviceLocalMemory = false;
for (uint32_t i = 0; i < memoryProps->memoryTypeCount; i++) {
if ((memoryProps->memoryTypes[i].propertyFlags & uploadHeapPropertyFlags) == uploadHeapPropertyFlags) {
hasHostVisibleDeviceLocalMemory = true;
}
}
for (uint32_t i = 0; i < memoryProps->memoryHeapCount; i++) {
if (memoryProps->memoryHeaps[i].flags & VK_MEMORY_HEAP_DEVICE_LOCAL_BIT) {
memoryHeapSize = std::max(memoryProps->memoryHeaps[i].size, memoryHeapSize);
@ -3907,6 +3922,8 @@ namespace plume {
capabilities.preferHDR = memoryHeapSize > (512 * 1024 * 1024);
capabilities.triangleFan = true;
capabilities.dynamicDepthBias = true;
capabilities.uma = (description.type == RenderDeviceType::INTEGRATED) && hasHostVisibleDeviceLocalMemory;
capabilities.gpuUploadHeap = capabilities.uma;
// Fill Vulkan-only capabilities.
loadStoreOpNoneSupported = supportedOptionalExtensions.find(VK_EXT_LOAD_STORE_OP_NONE_EXTENSION_NAME) != supportedOptionalExtensions.end();

View file

@ -43,7 +43,7 @@ void main(
out float4 oColor0 : COLOR0,
out float4 oColor1 : COLOR1)
{
oPos.xy = (iPosition0.xy - 0.5) * g_ViewportSize.zw * float2(2.0, -2.0) + float2(-1.0, 1.0);
oPos.xy = iPosition0.xy * g_ViewportSize.zw * float2(2.0, -2.0) + float2(-1.0, 1.0);
oPos.z = g_Z.x;
oPos.w = 1.0;
oTexCoord0 = iColor0.wxyz;

View file

@ -44,7 +44,7 @@ void main(
out float4 oColor0 : COLOR0,
out float4 oColor1 : COLOR1)
{
oPos.xy = (iPosition0.xy - 0.5) * g_ViewportSize.zw * float2(2.0, -2.0) + float2(-1.0, 1.0);
oPos.xy = iPosition0.xy * g_ViewportSize.zw * float2(2.0, -2.0) + float2(-1.0, 1.0);
oPos.z = g_Z.x;
oPos.w = 1.0;
oTexCoord0 = iColor0.wxyz;

View file

@ -4,6 +4,7 @@ Interpolators main(in VertexShaderInput In)
{
Interpolators Out;
Out.ProjPos = In.ObjPos;
Out.ProjPos.xy += g_HalfPixelOffset * Out.ProjPos.w;
Out.UV = In.UV;
return Out;
}

View file

@ -163,6 +163,8 @@ struct SharedConstants
uint32_t samplerIndices[16]{};
uint32_t booleans{};
uint32_t swappedTexcoords{};
float halfPixelOffsetX{};
float halfPixelOffsetY{};
float alphaThreshold{};
};
@ -680,7 +682,10 @@ static void DestructTempResources()
g_textureDescriptorAllocator.free(texture->descriptorIndex);
if (texture->patchedTexture != nullptr)
g_textureDescriptorAllocator.free(texture->patchedTexture->descriptorIndex);
g_textureDescriptorAllocator.free(texture->patchedTexture->descriptorIndex);
if (texture->recreatedCubeMapTexture != nullptr)
g_textureDescriptorAllocator.free(texture->recreatedCubeMapTexture->descriptorIndex);
texture->~GuestTexture();
break;
@ -1737,6 +1742,21 @@ bool Video::CreateHostDevice(const char *sdlVideoDriver)
ApplyLowEndDefaults();
}
const RenderSampleCounts colourSampleCount = g_device->getSampleCountsSupported(RenderFormat::R16G16B16A16_FLOAT);
const RenderSampleCounts depthSampleCount = g_device->getSampleCountsSupported(RenderFormat::D32_FLOAT);
const RenderSampleCounts commonSampleCount = colourSampleCount & depthSampleCount;
// Disable specific MSAA levels if they are not supported.
if ((commonSampleCount & RenderSampleCount::COUNT_2) == 0)
Config::AntiAliasing.InaccessibleValues.emplace(EAntiAliasing::MSAA2x);
if ((commonSampleCount & RenderSampleCount::COUNT_4) == 0)
Config::AntiAliasing.InaccessibleValues.emplace(EAntiAliasing::MSAA4x);
if ((commonSampleCount & RenderSampleCount::COUNT_8) == 0)
Config::AntiAliasing.InaccessibleValues.emplace(EAntiAliasing::MSAA8x);
// Set Anti-Aliasing to nearest supported level.
Config::AntiAliasing.SnapToNearestAccessibleValue(false);
g_queue = g_device->createCommandQueue(RenderCommandListType::DIRECT);
for (auto& commandList : g_commandLists)
@ -2124,40 +2144,54 @@ static void* LockVertexBuffer(GuestBuffer* buffer, uint32_t, uint32_t, uint32_t
return LockBuffer(buffer, flags);
}
static std::atomic<uint32_t> g_bufferUploadCount = 0;
template<typename T>
static void UnlockBuffer(GuestBuffer* buffer, bool useCopyQueue)
{
auto uploadBuffer = g_device->createBuffer(RenderBufferDesc::UploadBuffer(buffer->dataSize));
auto copyBuffer = [&](T* dest)
{
auto src = reinterpret_cast<const T*>(buffer->mappedMemory);
auto dest = reinterpret_cast<T*>(uploadBuffer->map());
auto src = reinterpret_cast<const T*>(buffer->mappedMemory);
for (size_t i = 0; i < buffer->dataSize; i += sizeof(T))
{
*dest = ByteSwap(*src);
++dest;
++src;
}
uploadBuffer->unmap();
if (useCopyQueue)
{
ExecuteCopyCommandList([&]
for (size_t i = 0; i < buffer->dataSize; i += sizeof(T))
{
g_copyCommandList->copyBufferRegion(buffer->buffer->at(0), uploadBuffer->at(0), buffer->dataSize);
});
*dest = ByteSwap(*src);
++dest;
++src;
}
};
if (useCopyQueue && g_capabilities.gpuUploadHeap)
{
copyBuffer(reinterpret_cast<T*>(buffer->buffer->map()));
buffer->buffer->unmap();
}
else
{
auto& commandList = g_commandLists[g_frame];
auto uploadBuffer = g_device->createBuffer(RenderBufferDesc::UploadBuffer(buffer->dataSize));
copyBuffer(reinterpret_cast<T*>(uploadBuffer->map()));
uploadBuffer->unmap();
commandList->barriers(RenderBarrierStage::COPY, RenderBufferBarrier(buffer->buffer.get(), RenderBufferAccess::WRITE));
commandList->copyBufferRegion(buffer->buffer->at(0), uploadBuffer->at(0), buffer->dataSize);
commandList->barriers(RenderBarrierStage::GRAPHICS, RenderBufferBarrier(buffer->buffer.get(), RenderBufferAccess::READ));
if (useCopyQueue)
{
ExecuteCopyCommandList([&]
{
g_copyCommandList->copyBufferRegion(buffer->buffer->at(0), uploadBuffer->at(0), buffer->dataSize);
});
}
else
{
auto& commandList = g_commandLists[g_frame];
g_tempBuffers[g_frame].emplace_back(std::move(uploadBuffer));
commandList->barriers(RenderBarrierStage::COPY, RenderBufferBarrier(buffer->buffer.get(), RenderBufferAccess::WRITE));
commandList->copyBufferRegion(buffer->buffer->at(0), uploadBuffer->at(0), buffer->dataSize);
commandList->barriers(RenderBarrierStage::GRAPHICS, RenderBufferBarrier(buffer->buffer.get(), RenderBufferAccess::READ));
g_tempBuffers[g_frame].emplace_back(std::move(uploadBuffer));
}
}
g_bufferUploadCount++;
}
template<typename T>
@ -2335,10 +2369,11 @@ static void DrawProfiler()
std::lock_guard lock(g_userHeap.physicalMutex);
physicalDiagnostics = o1heapGetDiagnostics(g_userHeap.physicalHeap);
}
ImGui::Text("Heap Allocated: %d MB", int32_t(diagnostics.allocated / (1024 * 1024)));
ImGui::Text("Physical Heap Allocated: %d MB", int32_t(physicalDiagnostics.allocated / (1024 * 1024)));
ImGui::Text("GPU Waits: %d", int32_t(g_waitForGPUCount));
ImGui::Text("Buffer Uploads: %d", int32_t(g_bufferUploadCount));
ImGui::NewLine();
ImGui::Text("Present Wait: %s", g_capabilities.presentWait ? "Supported" : "Unsupported");
@ -2354,6 +2389,7 @@ static void DrawProfiler()
ImGui::Text("Device Type: %s", DeviceTypeName(g_device->getDescription().type));
ImGui::Text("VRAM: %.2f MiB", (double)(g_device->getDescription().dedicatedVideoMemory) / (1024.0 * 1024.0));
ImGui::Text("UMA: %s", g_capabilities.uma ? "Supported" : "Unsupported");
ImGui::Text("GPU Upload Heap: %s", g_capabilities.gpuUploadHeap ? "Supported" : "Unsupported");
const char* sdlVideoDriver = SDL_GetCurrentVideoDriver();
if (sdlVideoDriver != nullptr)
@ -3034,10 +3070,15 @@ static GuestTexture* CreateTexture(uint32_t width, uint32_t height, uint32_t dep
return texture;
}
static RenderHeapType GetBufferHeapType()
{
return g_capabilities.gpuUploadHeap ? RenderHeapType::GPU_UPLOAD : RenderHeapType::DEFAULT;
}
static GuestBuffer* CreateVertexBuffer(uint32_t length)
{
auto buffer = g_userHeap.AllocPhysical<GuestBuffer>(ResourceType::VertexBuffer);
buffer->buffer = g_device->createBuffer(RenderBufferDesc::VertexBuffer(length, RenderHeapType::DEFAULT, RenderBufferFlag::INDEX));
buffer->buffer = g_device->createBuffer(RenderBufferDesc::VertexBuffer(length, GetBufferHeapType(), RenderBufferFlag::INDEX));
buffer->dataSize = length;
#ifdef _DEBUG
buffer->buffer->setName(fmt::format("Vertex Buffer {:X}", g_memory.MapVirtual(buffer)));
@ -3048,7 +3089,7 @@ static GuestBuffer* CreateVertexBuffer(uint32_t length)
static GuestBuffer* CreateIndexBuffer(uint32_t length, uint32_t, uint32_t format)
{
auto buffer = g_userHeap.AllocPhysical<GuestBuffer>(ResourceType::IndexBuffer);
buffer->buffer = g_device->createBuffer(RenderBufferDesc::IndexBuffer(length, RenderHeapType::DEFAULT));
buffer->buffer = g_device->createBuffer(RenderBufferDesc::IndexBuffer(length, GetBufferHeapType()));
buffer->dataSize = length;
buffer->format = ConvertFormat(format);
buffer->guestFormat = format;
@ -3104,8 +3145,6 @@ static void FlushViewport()
if (g_dirtyStates.viewport)
{
auto viewport = g_viewport;
viewport.x += 0.5f;
viewport.y += 0.5f;
if (viewport.minDepth > viewport.maxDepth)
std::swap(viewport.minDepth, viewport.maxDepth);
@ -3529,6 +3568,12 @@ static void SetFramebuffer(GuestSurface* renderTarget, GuestSurface* depthStenci
g_framebuffer = nullptr;
}
if (g_framebuffer != nullptr)
{
SetDirtyValue(g_dirtyStates.sharedConstants, g_sharedConstants.halfPixelOffsetX, 1.0f / float(g_framebuffer->getWidth()));
SetDirtyValue(g_dirtyStates.sharedConstants, g_sharedConstants.halfPixelOffsetY, -1.0f / float(g_framebuffer->getHeight()));
}
g_dirtyStates.renderTargetAndDepthStencil = settingForClear;
}
}
@ -3656,6 +3701,14 @@ static void SetTextureInRenderThread(uint32_t index, GuestTexture* texture)
SetDirtyValue(g_dirtyStates.sharedConstants, g_sharedConstants.texture3DIndices[index], texture != nullptr &&
viewDimension == RenderTextureViewDimension::TEXTURE_3D ? texture->descriptorIndex : TEXTURE_DESCRIPTOR_NULL_TEXTURE_3D);
// Check if there's a cubemap texture we recreated and assign it if it's valid. The shader will pick whichever is correct.
if (viewDimension == RenderTextureViewDimension::TEXTURE_2D && texture->recreatedCubeMapTexture != nullptr)
{
texture = texture->recreatedCubeMapTexture.get();
AddBarrier(texture, RenderTextureLayout::SHADER_READ);
viewDimension = texture->viewDimension;
}
SetDirtyValue(g_dirtyStates.sharedConstants, g_sharedConstants.textureCubeIndices[index], texture != nullptr &&
viewDimension == RenderTextureViewDimension::TEXTURE_CUBE ? texture->descriptorIndex : TEXTURE_DESCRIPTOR_NULL_TEXTURE_CUBE);
}
@ -5486,21 +5539,30 @@ static RenderFormat ConvertDXGIFormat(ddspp::DXGIFormat format)
}
}
static bool LoadTexture(GuestTexture& texture, const uint8_t* data, size_t dataSize, RenderComponentMapping componentMapping)
static bool LoadTexture(GuestTexture& texture, const uint8_t* data, size_t dataSize, RenderComponentMapping componentMapping, bool forceCubeMap = false)
{
ddspp::Descriptor ddsDesc;
if (ddspp::decode_header((unsigned char *)(data), ddsDesc) != ddspp::Error)
{
forceCubeMap &= (ddsDesc.type == ddspp::Texture2D) && (ddsDesc.arraySize == 1);
uint32_t arraySize = ddsDesc.type == ddspp::TextureType::Cubemap ? (ddsDesc.arraySize * 6) : ddsDesc.arraySize;
RenderTextureDesc desc;
desc.dimension = ConvertTextureDimension(ddsDesc.type);
desc.width = ddsDesc.width;
desc.height = ddsDesc.height;
desc.depth = ddsDesc.depth;
desc.mipLevels = ddsDesc.numMips;
desc.arraySize = ddsDesc.type == ddspp::TextureType::Cubemap ? ddsDesc.arraySize * 6 : ddsDesc.arraySize;
desc.arraySize = arraySize;
desc.format = ConvertDXGIFormat(ddsDesc.format);
desc.flags = ddsDesc.type == ddspp::TextureType::Cubemap ? RenderTextureFlag::CUBE : RenderTextureFlag::NONE;
if (forceCubeMap)
{
desc.arraySize = 6;
desc.flags = RenderTextureFlag::CUBE;
}
texture.textureHolder = g_device->createTexture(desc);
texture.texture = texture.textureHolder.get();
texture.layout = RenderTextureLayout::COPY_DEST;
@ -5510,6 +5572,10 @@ static bool LoadTexture(GuestTexture& texture, const uint8_t* data, size_t dataS
viewDesc.dimension = ConvertTextureViewDimension(ddsDesc.type);
viewDesc.mipLevels = ddsDesc.numMips;
viewDesc.componentMapping = componentMapping;
if (forceCubeMap)
viewDesc.dimension = RenderTextureViewDimension::TEXTURE_CUBE;
texture.textureView = texture.texture->createTextureView(viewDesc);
texture.descriptorIndex = g_textureDescriptorAllocator.allocate();
g_textureDescriptorSet->setTexture(texture.descriptorIndex, texture.texture, RenderTextureLayout::SHADER_READ, texture.textureView.get());
@ -5534,7 +5600,7 @@ static bool LoadTexture(GuestTexture& texture, const uint8_t* data, size_t dataS
uint32_t curSrcOffset = 0;
uint32_t curDstOffset = 0;
for (uint32_t arraySlice = 0; arraySlice < desc.arraySize; arraySlice++)
for (uint32_t arraySlice = 0; arraySlice < arraySize; arraySlice++)
{
for (uint32_t mipSlice = 0; mipSlice < ddsDesc.numMips; mipSlice++)
{
@ -5584,13 +5650,24 @@ static bool LoadTexture(GuestTexture& texture, const uint8_t* data, size_t dataS
{
g_copyCommandList->barriers(RenderBarrierStage::COPY, RenderTextureBarrier(texture.texture, RenderTextureLayout::COPY_DEST));
for (size_t i = 0; i < slices.size(); i++)
{
auto& slice = slices[i];
auto copyTextureRegion = [&](Slice& slice, uint32_t subresourceIndex)
{
g_copyCommandList->copyTextureRegion(
RenderTextureCopyLocation::Subresource(texture.texture, subresourceIndex),
RenderTextureCopyLocation::PlacedFootprint(uploadBuffer.get(), desc.format, slice.width, slice.height, slice.depth, (slice.dstRowPitch * 8) / ddsDesc.bitsPerPixelOrBlock * ddsDesc.blockWidth, slice.dstOffset));
};
g_copyCommandList->copyTextureRegion(
RenderTextureCopyLocation::Subresource(texture.texture, i),
RenderTextureCopyLocation::PlacedFootprint(uploadBuffer.get(), desc.format, slice.width, slice.height, slice.depth, (slice.dstRowPitch * 8) / ddsDesc.bitsPerPixelOrBlock * ddsDesc.blockWidth, slice.dstOffset));
for (size_t i = 0; i < slices.size(); i++)
copyTextureRegion(slices[i], i);
// Duplicate the first face across the remaining 6 faces.
if (forceCubeMap)
{
for (size_t i = 1; i < 6; i++)
{
for (size_t j = 0; j < slices.size(); j++)
copyTextureRegion(slices[j], (slices.size() * i) + j);
}
}
});
@ -5663,13 +5740,12 @@ std::unique_ptr<GuestTexture> LoadTexture(const uint8_t* data, size_t dataSize,
return nullptr;
}
static void DiffPatchTexture(GuestTexture& texture, uint8_t* data, uint32_t dataSize)
static void DiffPatchTexture(GuestTexture& texture, uint8_t* data, uint32_t dataSize, XXH64_hash_t hash)
{
auto header = reinterpret_cast<BlockCompressionDiffPatchHeader*>(g_buttonBcDiff.get());
auto entries = reinterpret_cast<BlockCompressionDiffPatchEntry*>(g_buttonBcDiff.get() + header->entriesOffset);
auto end = entries + header->entryCount;
XXH64_hash_t hash = XXH3_64bits(data, dataSize);
auto findResult = std::lower_bound(entries, end, hash, [](BlockCompressionDiffPatchEntry& lhs, XXH64_hash_t rhs)
{
return lhs.hash < rhs;
@ -5702,8 +5778,19 @@ static void MakePictureData(GuestPictureData* pictureData, uint8_t* data, uint32
#ifdef _DEBUG
texture.texture->setName(reinterpret_cast<char*>(g_memory.Translate(pictureData->name + 2)));
#endif
XXH64_hash_t hash = XXH3_64bits(data, dataSize);
DiffPatchTexture(texture, data, dataSize);
// The whale in Cool Edge has a 2D texture assigned as a cubemap which makes it not display in recomp.
// The hardware duplicates the first face to the remaining 6 faces, so to simulate that we'll recreate the asset.
bool forceCubeMap = (dataSize == 0xAB38) && (hash == 0x160E9E250FDE88A9);
if (forceCubeMap)
{
GuestTexture recreatedCubeMapTexture(ResourceType::Texture);
if (LoadTexture(recreatedCubeMapTexture, data, dataSize, {}, true))
texture.recreatedCubeMapTexture = std::make_unique<GuestTexture>(std::move(recreatedCubeMapTexture));
}
DiffPatchTexture(texture, data, dataSize, hash);
pictureData->texture = g_memory.MapVirtual(g_userHeap.AllocPhysical<GuestTexture>(std::move(texture)));
pictureData->type = 0;

View file

@ -158,6 +158,7 @@ struct GuestTexture : GuestBaseTexture
void* mappedMemory = nullptr;
std::unique_ptr<RenderFramebuffer> framebuffer;
std::unique_ptr<GuestTexture> patchedTexture;
std::unique_ptr<GuestTexture> recreatedCubeMapTexture;
struct GuestSurface* sourceSurface = nullptr;
};

View file

@ -310,6 +310,8 @@ void hid::Init()
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_PS5_RUMBLE, "1");
SDL_SetHint(SDL_HINT_JOYSTICK_HIDAPI_WII, "1");
SDL_SetHint(SDL_HINT_XINPUT_ENABLED, "1");
SDL_SetHint(SDL_HINT_GAMECONTROLLER_USE_BUTTON_LABELS, "0"); // Uses Button Labels. This hint is disabled for Nintendo Controllers.
SDL_InitSubSystem(SDL_INIT_EVENTS);
SDL_AddEventWatch(HID_OnSDLEvent, nullptr);

View file

@ -67,6 +67,73 @@ static std::unique_ptr<VirtualFileSystem> createFileSystemFromPath(const std::fi
}
}
static bool checkFile(const FilePair &pair, const uint64_t *fileHashes, const std::filesystem::path &targetDirectory, std::vector<uint8_t> &fileData, Journal &journal, const std::function<bool()> &progressCallback, bool checkSizeOnly) {
const std::string fileName(pair.first);
const uint32_t hashCount = pair.second;
const std::filesystem::path filePath = targetDirectory / fileName;
if (!std::filesystem::exists(filePath))
{
journal.lastResult = Journal::Result::FileMissing;
journal.lastErrorMessage = fmt::format("File {} does not exist.", fileName);
return false;
}
std::error_code ec;
size_t fileSize = std::filesystem::file_size(filePath, ec);
if (ec)
{
journal.lastResult = Journal::Result::FileReadFailed;
journal.lastErrorMessage = fmt::format("Failed to read file size for {}.", fileName);
return false;
}
if (checkSizeOnly)
{
journal.progressTotal += fileSize;
}
else
{
std::ifstream fileStream(filePath, std::ios::binary);
if (fileStream.is_open())
{
fileData.resize(fileSize);
fileStream.read((char *)(fileData.data()), fileSize);
}
if (!fileStream.is_open() || fileStream.bad())
{
journal.lastResult = Journal::Result::FileReadFailed;
journal.lastErrorMessage = fmt::format("Failed to read file {}.", fileName);
return false;
}
uint64_t fileHash = XXH3_64bits(fileData.data(), fileSize);
bool fileHashFound = false;
for (uint32_t i = 0; i < hashCount && !fileHashFound; i++)
{
fileHashFound = fileHash == fileHashes[i];
}
if (!fileHashFound)
{
journal.lastResult = Journal::Result::FileHashFailed;
journal.lastErrorMessage = fmt::format("File {} did not match any of the known hashes.", fileName);
return false;
}
journal.progressCounter += fileSize;
}
if (!progressCallback())
{
journal.lastResult = Journal::Result::Cancelled;
journal.lastErrorMessage = "Check was cancelled.";
return false;
}
return true;
}
static bool copyFile(const FilePair &pair, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, const std::filesystem::path &targetDirectory, bool skipHashChecks, std::vector<uint8_t> &fileData, Journal &journal, const std::function<bool()> &progressCallback) {
const std::string filename(pair.first);
const uint32_t hashCount = pair.second;
@ -204,6 +271,45 @@ static DLC detectDLC(const std::filesystem::path &sourcePath, VirtualFileSystem
}
}
static bool fillDLCSource(DLC dlc, Installer::DLCSource &dlcSource)
{
switch (dlc)
{
case DLC::Spagonia:
dlcSource.filePairs = { SpagoniaFiles, SpagoniaFilesSize };
dlcSource.fileHashes = SpagoniaHashes;
dlcSource.targetSubDirectory = SpagoniaDirectory;
return true;
case DLC::Chunnan:
dlcSource.filePairs = { ChunnanFiles, ChunnanFilesSize };
dlcSource.fileHashes = ChunnanHashes;
dlcSource.targetSubDirectory = ChunnanDirectory;
return true;
case DLC::Mazuri:
dlcSource.filePairs = { MazuriFiles, MazuriFilesSize };
dlcSource.fileHashes = MazuriHashes;
dlcSource.targetSubDirectory = MazuriDirectory;
return true;
case DLC::Holoska:
dlcSource.filePairs = { HoloskaFiles, HoloskaFilesSize };
dlcSource.fileHashes = HoloskaHashes;
dlcSource.targetSubDirectory = HoloskaDirectory;
return true;
case DLC::ApotosShamar:
dlcSource.filePairs = { ApotosShamarFiles, ApotosShamarFilesSize };
dlcSource.fileHashes = ApotosShamarHashes;
dlcSource.targetSubDirectory = ApotosShamarDirectory;
return true;
case DLC::EmpireCityAdabat:
dlcSource.filePairs = { EmpireCityAdabatFiles, EmpireCityAdabatFilesSize };
dlcSource.fileHashes = EmpireCityAdabatHashes;
dlcSource.targetSubDirectory = EmpireCityAdabatDirectory;
return true;
default:
return false;
}
}
bool Installer::checkGameInstall(const std::filesystem::path &baseDirectory, std::filesystem::path &modulePath)
{
modulePath = baseDirectory / PatchedDirectory / GameExecutableFile;
@ -254,6 +360,40 @@ bool Installer::checkAllDLC(const std::filesystem::path& baseDirectory)
return result;
}
bool Installer::checkInstallIntegrity(const std::filesystem::path &baseDirectory, Journal &journal, const std::function<bool()> &progressCallback)
{
// Run the file checks twice: once to fill out the progress counter and the file sizes, and another pass to do the hash integrity checks.
for (uint32_t checkPass = 0; checkPass < 2; checkPass++)
{
bool checkSizeOnly = (checkPass == 0);
if (!checkFiles({ GameFiles, GameFilesSize }, GameHashes, baseDirectory / GameDirectory, journal, progressCallback, checkSizeOnly))
{
return false;
}
if (!checkFiles({ UpdateFiles, UpdateFilesSize }, UpdateHashes, baseDirectory / UpdateDirectory, journal, progressCallback, checkSizeOnly))
{
return false;
}
for (int i = 1; i < (int)DLC::Count; i++)
{
if (checkDLCInstall(baseDirectory, (DLC)i))
{
Installer::DLCSource dlcSource;
fillDLCSource((DLC)i, dlcSource);
if (!checkFiles(dlcSource.filePairs, dlcSource.fileHashes, baseDirectory / dlcSource.targetSubDirectory, journal, progressCallback, checkSizeOnly))
{
return false;
}
}
}
}
return true;
}
bool Installer::computeTotalSize(std::span<const FilePair> filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, Journal &journal, uint64_t &totalSize)
{
for (FilePair pair : filePairs)
@ -272,6 +412,27 @@ bool Installer::computeTotalSize(std::span<const FilePair> filePairs, const uint
return true;
}
bool Installer::checkFiles(std::span<const FilePair> filePairs, const uint64_t *fileHashes, const std::filesystem::path &targetDirectory, Journal &journal, const std::function<bool()> &progressCallback, bool checkSizeOnly)
{
FilePair validationPair = {};
uint32_t validationHashIndex = 0;
uint32_t hashIndex = 0;
uint32_t hashCount = 0;
std::vector<uint8_t> fileData;
for (FilePair pair : filePairs)
{
hashIndex = hashCount;
hashCount += pair.second;
if (!checkFile(pair, &fileHashes[hashIndex], targetDirectory, fileData, journal, progressCallback, checkSizeOnly))
{
return false;
}
}
return true;
}
bool Installer::copyFiles(std::span<const FilePair> filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, const std::filesystem::path &targetDirectory, const std::string &validationFile, bool skipHashChecks, Journal &journal, const std::function<bool()> &progressCallback)
{
std::error_code ec;
@ -387,39 +548,8 @@ bool Installer::parseSources(const Input &input, Journal &journal, Sources &sour
}
DLC dlc = detectDLC(path, *dlcSource.sourceVfs, journal);
switch (dlc)
if (!fillDLCSource(dlc, dlcSource))
{
case DLC::Spagonia:
dlcSource.filePairs = { SpagoniaFiles, SpagoniaFilesSize };
dlcSource.fileHashes = SpagoniaHashes;
dlcSource.targetSubDirectory = SpagoniaDirectory;
break;
case DLC::Chunnan:
dlcSource.filePairs = { ChunnanFiles, ChunnanFilesSize };
dlcSource.fileHashes = ChunnanHashes;
dlcSource.targetSubDirectory = ChunnanDirectory;
break;
case DLC::Mazuri:
dlcSource.filePairs = { MazuriFiles, MazuriFilesSize };
dlcSource.fileHashes = MazuriHashes;
dlcSource.targetSubDirectory = MazuriDirectory;
break;
case DLC::Holoska:
dlcSource.filePairs = { HoloskaFiles, HoloskaFilesSize };
dlcSource.fileHashes = HoloskaHashes;
dlcSource.targetSubDirectory = HoloskaDirectory;
break;
case DLC::ApotosShamar:
dlcSource.filePairs = { ApotosShamarFiles, ApotosShamarFilesSize };
dlcSource.fileHashes = ApotosShamarHashes;
dlcSource.targetSubDirectory = ApotosShamarDirectory;
break;
case DLC::EmpireCityAdabat:
dlcSource.filePairs = { EmpireCityAdabatFiles, EmpireCityAdabatFilesSize };
dlcSource.fileHashes = EmpireCityAdabatHashes;
dlcSource.targetSubDirectory = EmpireCityAdabatDirectory;
break;
default:
return false;
}

View file

@ -75,7 +75,9 @@ struct Installer
static bool checkGameInstall(const std::filesystem::path &baseDirectory, std::filesystem::path &modulePath);
static bool checkDLCInstall(const std::filesystem::path &baseDirectory, DLC dlc);
static bool checkAllDLC(const std::filesystem::path &baseDirectory);
static bool checkInstallIntegrity(const std::filesystem::path &baseDirectory, Journal &journal, const std::function<bool()> &progressCallback);
static bool computeTotalSize(std::span<const FilePair> filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, Journal &journal, uint64_t &totalSize);
static bool checkFiles(std::span<const FilePair> filePairs, const uint64_t *fileHashes, const std::filesystem::path &targetDirectory, Journal &journal, const std::function<bool()> &progressCallback, bool checkSizeOnly);
static bool copyFiles(std::span<const FilePair> filePairs, const uint64_t *fileHashes, VirtualFileSystem &sourceVfs, const std::filesystem::path &targetDirectory, const std::string &validationFile, bool skipHashChecks, Journal &journal, const std::function<bool()> &progressCallback);
static bool parseContent(const std::filesystem::path &sourcePath, std::unique_ptr<VirtualFileSystem> &targetVfs, Journal &journal);
static bool parseSources(const Input &input, Journal &journal, Sources &sources);

View file

@ -383,7 +383,17 @@ std::filesystem::path FileSystem::ResolvePath(const std::string_view& path, bool
if (index != std::string::npos)
{
// rooted folder, handle direction
const std::string_view root = path.substr(0, index);
std::string_view root = path.substr(0, index);
// HACK: The game tries to load work folder from the "game" root path for
// Application and shader archives, which does not work in Recomp because
// we don't support stacking the update and game files on top of each other.
//
// We can fix it by redirecting it to update instead as we know the original
// game files don't have a work folder.
if (path.starts_with("game:\\work\\"))
root = "update";
const auto newRoot = XamGetRootPath(root);
if (!newRoot.empty())

View file

@ -142,7 +142,7 @@ CONFIG_DEFINE_LOCALE(ControlTutorial)
{ ELanguage::English, { "Control Tutorial", "Show controller hints during gameplay.\n\nThe Werehog Critical Attack prompt will be unaffected." } },
{ ELanguage::Japanese, { "アクションナビ", "ゲーム[内:ない]に\u200Bアクションナビを\u200B[表示:ひょうじ]するか\u200B[選択:せんたく]できます\n\n「チャンスアタック」の\u200B[表示:ひょうじ]は\u200B[影響:えいきょう]されません" } },
{ ELanguage::German, { "Steuerungsanleitung", "Zeige Steuerungshinweise während des Spiels.\n\nKritische Angriffe des Werehogs werden hiervon nicht beeinflusst." } },
{ ELanguage::French, { "Indication des commandes", "Affiche les indications des commandes pendant le jeu.\n\nCeci n'affecte pas les Coup critiques du Werehog." } },
{ ELanguage::French, { "Indication des commandes", "Affiche les indications des commandes pendant le jeu.\n\nCeci n'affecte pas les Coup critique du Werehog." } },
{ ELanguage::Spanish, { "Tutorial de controles", "Muestra pistas de controles durante el juego.\n\nEl indicador de ataque crítico del Werehog no se verá afectado." } },
{ ELanguage::Italian, { "Tutorial dei comandi", "Mostra i tutorial dei comandi durante il gioco.\n\nIl tutorial per l'attacco critico del Werehog non verrà influenzato da questa opzione." } }
};
@ -182,8 +182,8 @@ CONFIG_DEFINE_ENUM_LOCALE(ETimeOfDayTransition)
{
ELanguage::Japanese,
{
{ ETimeOfDayTransition::Xbox, { "XBOX", "Xbox: [変身:へんしん]シーン\u200B[人工的:じんこうてき]な\u200B[読:よ]み[込:こ]み[時間:じかん]で\u200B[再生:さいせい]されます" } },
{ ETimeOfDayTransition::PlayStation, { "PLAYSTATION", "PlayStation: [回転:かいてん]するメダル\u200B[読:よ]み[込:こ]み[画面:がめん]が\u200B[使用:しよう]されます" } }
{ ETimeOfDayTransition::Xbox, { "XBOX", "Xbox: [変身:へんしん]シーン\u200B[人工的:じんこうてき]な\u200B[読:よ]み[込:こ]み[時間:じかん]で\u200B[再生:さいせい]されます" } },
{ ETimeOfDayTransition::PlayStation, { "PLAYSTATION", "PlayStation: [回転:かいてん]するメダル\u200B[読:よ]み[込:こ]み[画面:がめん]が\u200B[使用:しよう]されます" } }
}
},
{
@ -196,8 +196,8 @@ CONFIG_DEFINE_ENUM_LOCALE(ETimeOfDayTransition)
{
ELanguage::French,
{
{ ETimeOfDayTransition::Xbox, { "XBOX", "Xbox: la scène de transformation sera jouée avec des temps de chargement artificiel." } },
{ ETimeOfDayTransition::PlayStation, { "PLAYSTATION", "PlayStation: un écran de chargement avec une médaille tournoyante sera utilisé à la place." } }
{ ETimeOfDayTransition::Xbox, { "XBOX", "Xbox : la scène de transformation sera jouée avec des temps de chargement artificiels." } },
{ ETimeOfDayTransition::PlayStation, { "PLAYSTATION", "PlayStation : un écran de chargement avec une médaille tournoyante sera utilisé à la place." } }
}
},
{
@ -220,7 +220,7 @@ CONFIG_DEFINE_ENUM_LOCALE(ETimeOfDayTransition)
CONFIG_DEFINE_LOCALE(ControllerIcons)
{
{ ELanguage::English, { "Controller Icons", "Change the icons to match your controller." } },
{ ELanguage::Japanese, { "コントローライコン", "ゲーム[内:ない]の\u200Bコントローライコン\u200B[変更:へんこう]できます" } },
{ ELanguage::Japanese, { "コントローラーアイコン", "ゲーム[内:ない]の\u200Bコントローラーアイコン\u200B[変更:へんこう]できます" } },
{ ELanguage::German, { "Controllersymbole", "Ändere die Controllersymbole, um sie auf dein Modell anzupassen." } },
{ ELanguage::French, { "Icône des boutons", "Modifie les icônes pour les faire correspondre à votre manette." } },
{ ELanguage::Spanish, { "Iconos del mando", "Cambia los iconos para que coincidan con tu mando." } },
@ -233,7 +233,7 @@ CONFIG_DEFINE_ENUM_LOCALE(EControllerIcons)
{
ELanguage::English,
{
{ EControllerIcons::Auto, { "AUTO", "Auto: the game will determine which icons to use based on the current input device." } },
{ EControllerIcons::Auto, { "AUTO", "Auto : the game will determine which icons to use based on the current input device." } },
{ EControllerIcons::Xbox, { "XBOX", "" } },
{ EControllerIcons::PlayStation, { "PLAYSTATION", "" } }
}
@ -241,7 +241,7 @@ CONFIG_DEFINE_ENUM_LOCALE(EControllerIcons)
{
ELanguage::Japanese,
{
{ EControllerIcons::Auto, { "自動検出", "[自動検出:じどうけんしゅつ]: ゲーム[内:ない]の\u200Bコントローライコンを\u200B[自動検出:じどうけんしゅつ]されます" } },
{ EControllerIcons::Auto, { "自動検出", "[\u2005自動検出\u2005:じどうけんしゅつ]: コントローラーアイコン\u200Bを[使用:しよう]している\u200B[\u2005\u2005\u2005:にゅうりょく]デバイスに\u200B[応:おう]じて\u200B[自動的:じどうてき]に\u200B[決定:けってい]します" } },
{ EControllerIcons::Xbox, { "XBOX", "" } },
{ EControllerIcons::PlayStation, { "PLAYSTATION", "" } }
}
@ -257,7 +257,7 @@ CONFIG_DEFINE_ENUM_LOCALE(EControllerIcons)
{
ELanguage::French,
{
{ EControllerIcons::Auto, { "AUTO", "Auto: le jeu déterminera automatiquement quelles icônes utiliser en fonction du périphérique d'entrée." } },
{ EControllerIcons::Auto, { "AUTO", "Auto : le jeu déterminera automatiquement quelles icônes utiliser en fonction du périphérique d'entrée." } },
{ EControllerIcons::Xbox, { "XBOX", "" } },
{ EControllerIcons::PlayStation, { "PLAYSTATION", "" } }
}
@ -354,7 +354,7 @@ CONFIG_DEFINE_LOCALE(Vibration)
{ ELanguage::English, { "Vibration", "Toggle controller vibration." } },
{ ELanguage::Japanese, { "[振動:しんどう]", "[振動:しんどう]の\u200B[有無:うむ]を\u200B[選択:せんたく]できます" } },
{ ELanguage::German, { "Vibration", "Schalte die Controllervibration an oder aus." } },
{ ELanguage::French, { "Vibration", "Active les vibrations manette." } },
{ ELanguage::French, { "Vibration", "Active les vibrations de la manette." } },
{ ELanguage::Spanish, { "Vibración", "Activa o desactiva la vibración del mando." } },
{ ELanguage::Italian, { "Vibrazione", "Attiva/disattiva la vibrazione del controller." } }
};
@ -407,7 +407,7 @@ CONFIG_DEFINE_LOCALE(EffectsVolume)
CONFIG_DEFINE_LOCALE(MusicAttenuation)
{
{ ELanguage::English, { "Music Attenuation", "Fade out the game's music when external media is playing." } },
{ ELanguage::Japanese, { "BGM[減衰:げんすい]", "[外部:がいぶ]メディアを\u200B[再生:さいせい]すると\u200Bゲームの\u200B[音楽:おんがく]を\u200Bフェードアウトされます" } },
{ ELanguage::Japanese, { "BGM[減衰:げんすい]", "[外部:がいぶ]メディアを\u200B[再生:さいせい]すると\u200Bゲームの\u200B[音楽:おんがく]を\u200Bフェードアウトます" } },
{ ELanguage::German, { "Musikdämpfung", "Stelle die Musik des Spiels stumm während externe Medien abgespielt werden." } },
{ ELanguage::French, { "Atténuation audio", "Abaisse le volume des musiques du jeu lorsqu'un média externe est en cours de lecture." } },
{ ELanguage::Spanish, { "Atenuación de música", "Atenúa la música del juego cuando un reproductor multimedia se encuentra activo." } },
@ -497,9 +497,9 @@ CONFIG_DEFINE_LOCALE(Subtitles)
CONFIG_DEFINE_LOCALE(BattleTheme)
{
{ ELanguage::English, { "Battle Theme", "Play the Werehog battle theme during combat.\n\nThis option will apply the next time you're in combat.\n\nExorcism missions and miniboss themes will be unaffected." } },
{ ELanguage::Japanese, { "バトルテーマ", "バトル[中:ちゅう]に\u200Bウェアホッグの\u200Bバトルテーマを\u200B[再生:さいせい]するか\u200B[選択:せんたく]できます\n\nこのオプションは\u200B[次回:じかい]のバトル\u200B[適用:てきよう]されます\n\n[\u2005\u2005\u2005:エクソシズム]ミッションと\u200Bミニボステーマ\u200B[影響:えいきょう]されません" } },
{ ELanguage::Japanese, { "バトルテーマ", "バトル[中:ちゅう]に\u200Bウェアホッグの\u200Bバトルテーマを\u200B[再生:さいせい]するか\u200B[選択:せんたく]できます\n\nこのオプションは\u200B[次回:じかい]のバトルから\u200B[適用:てきよう]されます\n\n[\u2005\u2005\u2005:エクソシズム]ミッションと\u200Bミニボステーマには\u200B[\u2005影響\u2005:えいきょう]しません" } },
{ ELanguage::German, { "Kampfmusik", "Spiele die Kampfmusik des Werehogs während dem Kämpfen ab.\n\nDiese Option tritt das nächste Mal, wenn du in einen Kampf gerätst, in Kraft.\n\nExorzismen und Mini-Bosse werden hiervon nicht beeinflusst." } },
{ ELanguage::French, { "Thème de combat", "Joue le thème de combat du Werehog pendant les combat.\n\nCette option s'appliquera la prochaine fois que vous serez en combat.\n\nLes missions d'exorcisme et les thèmes des miniboss ne seront pas affectés." } },
{ ELanguage::French, { "Thème de combat", "Joue le thème de combat du Werehog pendant ces derniers.\n\nCette option s'appliquera la prochaine fois que vous serez en combat.\n\nLes missions d'exorcisme et les thèmes des miniboss ne seront pas affectés." } },
{ ELanguage::Spanish, { "Tema de batalla", "Reproduce el tema de batalla del Werehog durante el combate.\n\nEsta opción se aplicará la próxima vez que entres en combate.\n\nLas misiones de exorcismo y los temas de los minijefes no se verán afectados." } },
{ ELanguage::Italian, { "Musica di combattimento", "Riproduci la musica di combattimento del Werehog quando inizi una battaglia.\n\nQuesta opzione verrà applicata la prossima volta che sei in battaglia.\n\nLa traccia musicale verrà riprodotta ugualmente nelle missioni di Esorcismo e i miniboss." } }
};
@ -508,7 +508,7 @@ CONFIG_DEFINE_LOCALE(BattleTheme)
CONFIG_DEFINE_LOCALE(WindowSize)
{
{ ELanguage::English, { "Window Size", "Adjust the size of the game window in windowed mode." } },
{ ELanguage::Japanese, { "ウィンドウサイズ", "ウィンドウモードで\u200Bゲームの\u200Bウィンドウサイズを\u200B[調整:ちょうせい]できます" } },
{ ELanguage::Japanese, { "ウィンドウサイズ", "ウィンドウモードで\u200Bゲームの\u200Bウィンドウサイズを\u200B[調整:ちょうせい]できます" } },
{ ELanguage::German, { "Fenstergröße", "Ändere die Größe des Spielfensters im Fenstermodus." } },
{ ELanguage::French, { "Taille de la fenêtre", "Modifie la taille de la fenêtre de jeu en mode fenêtré." } },
{ ELanguage::Spanish, { "Tamaño de ventana", "Ajusta el tamaño de la ventana de juego." } },
@ -552,7 +552,7 @@ CONFIG_DEFINE_ENUM_LOCALE(EAspectRatio)
{
ELanguage::Japanese,
{
{ EAspectRatio::Auto, { "自動検出", "[自動検出:じどうけんしゅつ]: ゲーム[内:ない]の\u200Bコントローライコンを\u200B[自動検出:じどうけんしゅつ]されます" } },
{ EAspectRatio::Auto, { "自動", "[自動:じどう]: アスペクト[比:ひ]は\u200Bウィンドウサイズに\u200B[合:あ]わせて\u200B[調整:ちょうせい]されます" } },
{ EAspectRatio::Wide, { "16:9", "16:9: ワイドスクリーンの\u200Bアスペクト[比:ひ]に\u200B[固定:こてい]されます" } },
{ EAspectRatio::Narrow, { "4:3", "4:3: ナローの\u200Bアスペクト[比:ひ]に\u200B[固定:こてい]されます" } },
{ EAspectRatio::OriginalNarrow, { "オリジナル 4:3", "オリジナル 4:3: オリジナルの\u200Bアスペクト[比:ひ]に\u200B[固定:こてい]されます" } }
@ -570,10 +570,10 @@ CONFIG_DEFINE_ENUM_LOCALE(EAspectRatio)
{
ELanguage::French,
{
{ EAspectRatio::Auto, { "AUTO", "Auto: le format d'image s'adapte automatiquement à la taille de la fenêtre." } },
{ EAspectRatio::Wide, { "16:9", "16:9: force le jeu sur un format d'image large." } },
{ EAspectRatio::Narrow, { "4:3", "4:3: force le jeu sur un format d'image carré." } },
{ EAspectRatio::OriginalNarrow, { "4:3 ORIGINAL", "4:3 Original: force le jeu à un format d'image carré et conserve la parité avec l'implémentation d'origine du jeu." } }
{ EAspectRatio::Auto, { "AUTO", "Auto : le format d'image s'adapte automatiquement à la taille de la fenêtre." } },
{ EAspectRatio::Wide, { "16:9", "16:9 : force le jeu sur un format d'image large." } },
{ EAspectRatio::Narrow, { "4:3", "4:3 : force le jeu sur un format d'image carré." } },
{ EAspectRatio::OriginalNarrow, { "4:3 ORIGINAL", "4:3 original : force le jeu à un format d'image carré et conserve la parité avec l'implémentation d'origine du jeu." } }
}
},
{
@ -613,7 +613,7 @@ CONFIG_DEFINE_LOCALE(Fullscreen)
{ ELanguage::English, { "Fullscreen", "Toggle between borderless fullscreen or windowed mode." } },
{ ELanguage::Japanese, { "フルスクリーン", "ボーダーレス\u200Bフルスクリーンか\u200Bウィンドウモードを\u200B[選択:せんたく]できます" } },
{ ELanguage::German, { "Vollbild", "Wechsle zwischen dem randlosen Vollbildmodus und dem Fenstermodus." } },
{ ELanguage::French, { "Plein écran", "Alterne entre le mode plein écran sans bordure et le mode fenêtré." } },
{ ELanguage::French, { "Plein écran", "Alterne entre le mode plein écran sans bordures et le mode fenêtré." } },
{ ELanguage::Spanish, { "Pantalla completa", "Cambia entre modo de pantalla completa o ventana." } },
{ ELanguage::Italian, { "Schermo pieno", "Attiva/disattiva tra modalità finestra senza cornice e modalità finestra." } }
};
@ -635,7 +635,7 @@ CONFIG_DEFINE_LOCALE(FPS)
{ ELanguage::English, { "FPS", "Set the max frame rate the game can run at.\n\nWARNING: this may introduce glitches at frame rates higher than 60 FPS." } },
{ ELanguage::Japanese, { "フレームレート[上限:じょうげん]", "ゲームの\u200B[最大:さいだい]フレームレートを\u200B[設定:せってい]できます\n\n[警告:けいこく]: 60 FPSを\u200B[超:こ]えるフレームレートで\u200B[不具合:ふぐあい]が\u200B[発生:はっせい]する\u200B[可能性:かのうせい]が\u200Bあります" } },
{ ELanguage::German, { "FPS", "Setze die maximale Anzahl der Bilder pro Sekunde, die das Spiel darstellen kann.\n\nWARNUNG: Das Spiel kann bei höheren FPS als 60 ungewolltes Verhalten aufweisen." } },
{ ELanguage::French, { "FPS", "Détermine la fréquence d'images maximale du jeu.\n\nATTENTION: cela peut entraîner des problèmes à des taux de rafraîchissement supérieurs à 60 FPS." } },
{ ELanguage::French, { "IPS", "Détermine la fréquence d'images maximale du jeu.\n\nATTENTION : cela peut entraîner des problèmes à des taux de rafraîchissement supérieurs à 60 IPS." } },
{ ELanguage::Spanish, { "FPS", "Establece la tasa de fotogramas máxima a la que puede correr el juego.\n\nADVERTENCIA: esto puede introducir fallos en tasas mayores a 60 FPS." } },
{ ELanguage::Italian, { "FPS", "Imposta il frame rate massimo del gioco.\n\nATTENZIONE: questa opzione può causare dei glitch a frame rate più alti di 60 FPS." } }
};
@ -644,9 +644,9 @@ CONFIG_DEFINE_LOCALE(FPS)
CONFIG_DEFINE_LOCALE(Brightness)
{
{ ELanguage::English, { "Brightness", "Adjust the brightness level until the symbol on the left is barely visible." } },
{ ELanguage::Japanese, { "[明:か]るさの[設定:せってい]", "[画面:がめん]の\u200B[明:か]るさを\u200B[調整:ちょうせい]できます" } },
{ ELanguage::Japanese, { "[明:か]るさの[設定:せってい]", "[画面:がめん]の\u200B[明:か]るさを\u200B[調整:ちょうせい]できます" } },
{ ELanguage::German, { "Helligkeit", "Passe die Helligkeit des Spiels an bis das linke Symbol noch gerade so sichtbar ist." } },
{ ELanguage::French, { "Luminosité", "Règle le niveau de luminosité jusqu'à ce que le symbole à gauche soit à peine visible." } },
{ ELanguage::French, { "Luminosité", "Réglez le niveau de luminosité jusqu'à ce que le symbole à gauche soit à peine visible." } },
{ ELanguage::Spanish, { "Brillo", "Ajusta el nivel de brillo hasta que el símbolo a la izquierda sea apenas visible." } },
{ ELanguage::Italian, { "Luminosità", "Regola la luminosità dello schermo fino a quando il simbolo a sinistra diventa leggermente visibile." } }
};
@ -706,7 +706,7 @@ CONFIG_DEFINE_ENUM_LOCALE(EAntiAliasing)
CONFIG_DEFINE_LOCALE(TransparencyAntiAliasing)
{
{ ELanguage::English, { "Transparency Anti-Aliasing", "Apply anti-aliasing to alpha transparent textures." } },
{ ELanguage::Japanese, { "アンチエイリアスのトランスペアレンシー", "アルファ[透明:とうめい]\u200Bテクスチャに\u200Bアンチエイリアシングを\u200B[適用:てきよう]されます" } },
{ ELanguage::Japanese, { "[透明度:とうめいど]のアンチエイリアス", "[透過:とうか]テクスチャに\u200Bアンチエイリアスを\u200B[適用:てきよう]します" } },
{ ELanguage::German, { "Transparenz-Kantenglättung", "Wende Kantenglättung auf Alpha-Transparenz-Texturen an." } },
{ ELanguage::French, { "Anticrénelage de transparence", "Applique l'anticrénelage sur les textures transparentes." } },
{ ELanguage::Spanish, { "Anti-Aliasing de transparencias", "Aplica antialiasing a las texturas transparentes." } },
@ -719,7 +719,7 @@ CONFIG_DEFINE_LOCALE(ShadowResolution)
{ ELanguage::English, { "Shadow Resolution", "Set the resolution of real-time shadows." } },
{ ELanguage::Japanese, { "[影:かげ]の[解像度:かいぞうど]", "[影:かげ]の[解像度:かいぞうど]を\u200B[設定:せってい]できます" } },
{ ELanguage::German, { "Schattenauflösung", "Stelle die Auflösung der Echtzeit-Schatten ein." } },
{ ELanguage::French, { "Résolution des ombres", "Défini la résolution des ombres en temps réel." } },
{ ELanguage::French, { "Résolution des ombres", "Définit la résolution des ombres en temps réel." } },
{ ELanguage::Spanish, { "Resolución de sombras", "Establece la resolución de las sombras de tiempo real." } },
{ ELanguage::Italian, { "Risoluzione ombre", "Imposta la risoluzioni delle ombre in tempo reale." } }
};
@ -736,7 +736,7 @@ CONFIG_DEFINE_ENUM_LOCALE(EShadowResolution)
{
ELanguage::Japanese,
{
{ EShadowResolution::Original, { "オリジナル", "オリジナル: [影:かげ]の[解像度:かいぞうど]\u200B[自動検出:じどうけんしゅつ]されます" } }
{ EShadowResolution::Original, { "オリジナル", "オリジナル: [影:かげ]の[解像度:かいぞうど]\u200B[自動的:じどうてき]に\u200B[決定:けってい]されます" } }
}
},
{
@ -748,7 +748,7 @@ CONFIG_DEFINE_ENUM_LOCALE(EShadowResolution)
{
ELanguage::French,
{
{ EShadowResolution::Original, { "ORIGINALE", "Originale: le jeu déterminera automatiquement la résolution des ombres." } }
{ EShadowResolution::Original, { "ORIGINALE", "Originale : le jeu déterminera automatiquement la résolution des ombres." } }
}
},
{
@ -849,7 +849,7 @@ CONFIG_DEFINE_ENUM_LOCALE(EMotionBlur)
{
{ EMotionBlur::Off, { "オフ", "" } },
{ EMotionBlur::Original, { "オリジナル", "" } },
{ EMotionBlur::Enhanced, { "エンハンスド", "エンハンスド: パフォーマンスを\u200B[犠牲:ぎせい]にして\u200Bより[多:おお]くの\u200Bサンプルを\u200B[使用:しよう]してより\u200B[滑:なめ]らかな\u200Bモーションブラーを\u200B[実現:じつげん]されます" } }
{ EMotionBlur::Enhanced, { "エンハンスド", "エンハンスド: パフォーマンスを\u200B[犠牲:ぎせい]にして\u200Bより[多:おお]くの\u200Bサンプルを\u200B[使用:しよう]してより\u200B[滑:なめ]らかな\u200Bモーションブラーを\u200B[実現:じつげん]ます" } }
}
},
{
@ -865,7 +865,7 @@ CONFIG_DEFINE_ENUM_LOCALE(EMotionBlur)
{
{ EMotionBlur::Off, { "AUCUN", "" } },
{ EMotionBlur::Original, { "ORIGINAL", "" } },
{ EMotionBlur::Enhanced, { "AMÉLIORÉ", "Amélioré: utilise plus d'échantillons pour un flou de mouvement plus lisse au détriment des performances." } }
{ EMotionBlur::Enhanced, { "AMÉLIORÉ", "Amélioré : utilise plus d'échantillons pour un flou de mouvement plus lisse au détriment des performances." } }
}
},
{
@ -890,7 +890,7 @@ CONFIG_DEFINE_ENUM_LOCALE(EMotionBlur)
CONFIG_DEFINE_LOCALE(XboxColorCorrection)
{
{ ELanguage::English, { "Xbox Color Correction", "Use the warm tint from the Xbox version of the game." } },
{ ELanguage::Japanese, { "Xboxの[色補正:いろほせい]", "Xbox[版:ばん]のゲームの\u200B[暖色系:だんしょくけい]の[色合:いろあ]いを\u200B[使用:しよう]されます" } },
{ ELanguage::Japanese, { "Xboxの[色補正:いろほせい]", "Xbox[版:ばん]のゲームの\u200B[暖色系:だんしょくけい]の[色合:いろあ]いを\u200B[使用:しよう]ます" } },
{ ELanguage::German, { "Xbox Farbkorrektur", "Benutzt den warmen Farbstich aus der Xbox-Version des Spiels." } },
{ ELanguage::French, { "Correction couleurs Xbox", "Utilise le filtre de couleur provenant de la version Xbox du jeu." } },
{ ELanguage::Spanish, { "Corrección de color de Xbox", "Utiliza el tono cálido de la versión Xbox del juego." } },
@ -921,8 +921,8 @@ CONFIG_DEFINE_ENUM_LOCALE(ECutsceneAspectRatio)
{
ELanguage::Japanese,
{
{ ECutsceneAspectRatio::Original, { "オリジナル", "オリジナル: カットシーンを\u200B[元:もと]の\u200B16:9の\u200Bアスペクト[比:ひ]に\u200B[固定:こてい]されます" } },
{ ECutsceneAspectRatio::Unlocked, { "解除", "[解除:かいじょ]: カットシーンの\u200Bアスペクト[比:ひ]を\u200Bウィンドウサイズに\u200B[合:あ]わせて\u200B[調整:ちょうせい]されます\n\n[警告:けいこく]: [元:もと]の\u200B16:9の\u200Bアスペクト[比:ひ]を\u200B[超:こ]えると\u200B[視覚的:しかくてき]な\u200B[異常:いじょう]が\u200B[発生:はっせい]します" } },
{ ECutsceneAspectRatio::Original, { "オリジナル", "オリジナル: カットシーンを\u200B[元:もと]の\u200B16:9の\u200Bアスペクト[比:ひ]に\u200B[固定:こてい]ます" } },
{ ECutsceneAspectRatio::Unlocked, { "解除", "[解除:かいじょ]: カットシーンの\u200Bアスペクト[比:ひ]を\u200Bウィンドウサイズに\u200B[合:あ]わせて\u200B[調整:ちょうせい]ます\n\n[警告:けいこく]: [元:もと]の\u200B16:9の\u200Bアスペクト[比:ひ]を\u200B[超:こ]えると\u200B[視覚的:しかくてき]な\u200B[異常:いじょう]が\u200B[発生:はっせい]します" } },
}
},
{
@ -935,8 +935,8 @@ CONFIG_DEFINE_ENUM_LOCALE(ECutsceneAspectRatio)
{
ELanguage::French,
{
{ ECutsceneAspectRatio::Original, { "ORIGINAL", "Original: force les cinématiques dans leur format 16:9 d'origine." } },
{ ECutsceneAspectRatio::Unlocked, { "LIBRE", "Libre: permet aux cinématiques d'adapter leur format d'image à la taille de la fenêtre.\n\nAttention: au dela du format 16:9 d'origine, des bugs visuels apparaitront." } },
{ ECutsceneAspectRatio::Original, { "ORIGINAL", "Original : force les cinématiques dans leur format 16:9 d'origine." } },
{ ECutsceneAspectRatio::Unlocked, { "LIBRE", "Libre : permet aux cinématiques d'adapter leur format d'image à la taille de la fenêtre.\n\nATTENTION : au delà du format 16:9 d'origine, des bugs visuels apparaitront." } },
}
},
{
@ -979,8 +979,8 @@ CONFIG_DEFINE_ENUM_LOCALE(EUIAlignmentMode)
{
ELanguage::Japanese,
{
{ EUIAlignmentMode::Edge, { "エッジ", "エッジ: UIディスプレイの\u200B[端:はし]に\u200B[揃:そろ]います" } },
{ EUIAlignmentMode::Centre, { "センター", "センター: UIディスプレイの\u200B[中央:ちゅうおう]に\u200B[揃:そろ]います" } },
{ EUIAlignmentMode::Edge, { "エッジ", "エッジ: UIディスプレイの\u200B[端:はし]に\u200B[揃:そろ]います" } },
{ EUIAlignmentMode::Centre, { "センター", "センター: UIディスプレイの\u200B[中央:ちゅうおう]に\u200B[揃:そろ]います" } },
}
},
{
@ -993,8 +993,8 @@ CONFIG_DEFINE_ENUM_LOCALE(EUIAlignmentMode)
{
ELanguage::French,
{
{ EUIAlignmentMode::Edge, { "BORD", "Bord: l'interface utilisateur sera alignée sur les bords de l'écran." } },
{ EUIAlignmentMode::Centre, { "CENTRÉE", "Centrée: l'interface utilisateur sera alignée sur le centre de l'écran." } },
{ EUIAlignmentMode::Edge, { "BORD", "Bord : l'interface utilisateur sera alignée sur les bords de l'écran." } },
{ EUIAlignmentMode::Centre, { "CENTRÉE", "Centrée : l'interface utilisateur sera alignée sur le centre de l'écran." } },
}
},
{

View file

@ -145,7 +145,7 @@ std::unordered_map<std::string_view, std::unordered_map<ELanguage, std::string>>
"Options_Desc_NotAvailable",
{
{ ELanguage::English, "This option is not available at this location." },
{ ELanguage::Japanese, "この\u200Bオプションは\u200B[現在:げんざい]の\u200B[画面:がめん]で\u200B[変更:へんこう]\u200Bできません" },
{ ELanguage::Japanese, "この\u200Bオプションは\u200B[現在:げんざい]の\u200B[画面:がめん]で\u200B[変更:へんこう]\u200Bできません" },
{ ELanguage::German, "Diese Option ist an dieser Stelle nicht verfügbar." },
{ ELanguage::French, "Cette option n'est pas disponible pour l'instant." },
{ ELanguage::Spanish, "Esta opción no está disponible en este momento." },
@ -158,7 +158,7 @@ std::unordered_map<std::string_view, std::unordered_map<ELanguage, std::string>>
"Options_Desc_NotAvailableFullscreen",
{
{ ELanguage::English, "This option is not available in fullscreen mode." },
{ ELanguage::Japanese, "この\u200Bオプションは\u200Bフルスクリーンモードで\u200B[変更:へんこう]\u200Bできません" },
{ ELanguage::Japanese, "この\u200Bオプションは\u200Bフルスクリーンモードで\u200B[変更:へんこう]\u200Bできません" },
{ ELanguage::German, "Diese Option ist im Vollbildmodus nicht verfügbar." },
{ ELanguage::French, "Cette option n'est pas disponible en mode plein écran." },
{ ELanguage::Spanish, "Esta opción no está disponible en modo pantalla completa." },
@ -171,7 +171,7 @@ std::unordered_map<std::string_view, std::unordered_map<ELanguage, std::string>>
"Options_Desc_NotAvailableWindowed",
{
{ ELanguage::English, "This option is not available in windowed mode." },
{ ELanguage::Japanese, "この\u200Bオプションは\u200Bウィンドウモードで\u200B[変更:へんこう]\u200Bできません" },
{ ELanguage::Japanese, "この\u200Bオプションは\u200Bウィンドウモードで\u200B[変更:へんこう]\u200Bできません" },
{ ELanguage::German, "Diese Option ist im Fenstermodus nicht verfügbar." },
{ ELanguage::French, "Cette option n'est pas disponible en mode fenêtré." },
{ ELanguage::Spanish, "Esta opción no está disponible en modo ventana." },
@ -304,7 +304,7 @@ std::unordered_map<std::string_view, std::unordered_map<ELanguage, std::string>>
"Installer_Page_Introduction",
{
{ ELanguage::English, "Welcome to\nUnleashed Recompiled!\n\nYou'll need an Xbox 360 copy\nof Sonic Unleashed in order to proceed with the installation." },
{ ELanguage::Japanese, "Unleashed Recompiledへようこそ\nインストールにはXbox 360[版:ばん]の\n「ソニック ワールドアドベンチャー」\nが必要です" },
{ ELanguage::Japanese, "Unleashed Recompiledへようこそ\nインストールには\n[北米版:ほくべいばん]またはEU[版:ばん]のXbox 360[用:よう]\n「SONIC UNLEASHED」が[必要:ひつよう]です" },
{ ELanguage::German, "Willkommen zu\nUnleashed Recompiled!\nEs wird eine Xbox 360 Kopie von Sonic Unleashed benötigt um mit der Installation fortfahren zu können." },
{ ELanguage::French, "Bienvenue sur\nUnleashed Recompiled !\n\nVous aurez besoin d'une copie de Sonic Unleashed pour Xbox\n360 pour procéder à l'installation." },
{ ELanguage::Spanish, "¡Bienvenido a\nUnleashed Recompiled!\n\nNecesitas una copia de\nSonic Unleashed de Xbox 360\npara continuar con la instalación." },
@ -366,7 +366,7 @@ std::unordered_map<std::string_view, std::unordered_map<ELanguage, std::string>>
{ ELanguage::English, "Installation complete!\nThis project is brought to you by:" },
{ ELanguage::Japanese, "インストール[完了:かんりょう]\nプロジェクト[制作:せいさく]" },
{ ELanguage::German, "Installation abgeschlossen!\nDieses Projekt wird präsentiert von:" },
{ ELanguage::French, "Installation terminée !\nCe projet vous est présenté par:" },
{ ELanguage::French, "Installation terminée !\nCe projet vous est présenté par :" },
{ ELanguage::Spanish, "¡Instalación completada!\nEste proyecto ha sido posible gracias a:" },
{ ELanguage::Italian, "Installazione completata!\nQuesto progetto è stato creato da:" }
}
@ -714,6 +714,28 @@ std::unordered_map<std::string_view, std::unordered_map<ELanguage, std::string>>
{ ELanguage::Italian, "Impossibile allocare la memoria per il gioco.\n\nAssicurati che:\n\n- Soddisfi i requisiti minimi di sistema (8 GB).\n- Il tuo file di paging sia configurato con almeno 4 o 8 GB di memoria virtuale." }
}
},
{
"IntegrityCheck_Success",
{
{ ELanguage::English, "Installation check has finished.\n\nAll files seem to be correct.\n\nThe game will now close. Remove the launch argument to play the game." },
{ ELanguage::Japanese, "インストールチェックが終了しました\n\nすべてのファイルは正しいようです\n\nゲームは終了します、ゲームをプレイするには起動引数を削除してください" },
{ ELanguage::German, "Die Installation wurde überprüft.\n\nAlle Dateien scheinen korrekt zu sein.\n\nDas Spiel wird nun geschlossen. Entferne die Startoption, um das Spiel zu spielen." },
{ ELanguage::French, "La vérification de l'installation est terminée.\n\nTous les fichiers semblent corrects.\n\nL'application va maintenant se fermer. Retirez l'argument de lancement pour pouvoir lancer le jeu." },
{ ELanguage::Spanish, "La verificación de la instalación ha terminado.\n\nTodos los archivos parecen correctos.\n\nEl juego se cerrará ahora. Elimina el argumento de lanzamiento para jugar al juego." },
{ ELanguage::Italian, "La verifica dei file d'installazione è terminata.\n\nTutti i file sembrano corretti.\n\nIl gioco si chiuderà. Rimuovi l'argomento di avvio per poter giocare." }
}
},
{
"IntegrityCheck_Failed",
{
{ ELanguage::English, "Installation check has failed.\n\nError: %s\n\nThe game will now close. Try reinstalling the game by using the --install launch argument." },
{ ELanguage::Japanese, "インストールチェックに失敗しました\n\nエラー:%s\n\nゲームは終了します、--install 起動引数を使用してゲームを再インストールしてください" },
{ ELanguage::German, "Die Installationsprüfung ist fehlgeschlagen.\n\nFehler: %s\n\nDas Spiel wird nun geschlossen. Versuche das Spiel durch Verwendung der Startoption --install neu zu installieren." },
{ ELanguage::French, "La vérification de l'installation a échoué.\n\nErreur : %s\n\nL'application va maintenant se fermer. Essayez de réinstaller le jeu en utilisant l'argument de lancement --install." },
{ ELanguage::Spanish, "La verificación de la instalación ha fallado.\n\nError: %s\n\nEl juego se cerrará ahora. Intenta reinstalar el juego utilizando el argumento de lanzamiento --install." },
{ ELanguage::Italian, "La verifica dei file d'installazione non è andata a buon fine.\n\nErrore: %s\n\nIl gioco si chiuderà. Prova a reinstallare il gioco utilizzando l'argomento di avvio --install." }
}
},
{
"Common_On",
{

View file

@ -13,6 +13,7 @@
#include <hid/hid.h>
#include <user/config.h>
#include <user/paths.h>
#include <user/persistent_storage_manager.h>
#include <user/registry.h>
#include <kernel/xdbf.h>
#include <install/installer.h>
@ -23,6 +24,7 @@
#include <ui/game_window.h>
#include <ui/installer_wizard.h>
#include <mod/mod_loader.h>
#include <preload_executable.h>
#ifdef _WIN32
#include <timeapi.h>
@ -199,14 +201,21 @@ int main(int argc, char *argv[])
os::logger::Init();
PreloadContext preloadContext;
preloadContext.PreloadExecutable();
bool forceInstaller = false;
bool forceDLCInstaller = false;
bool useDefaultWorkingDirectory = false;
bool forceInstallationCheck = false;
const char *sdlVideoDriver = nullptr;
for (uint32_t i = 1; i < argc; i++)
{
forceInstaller = forceInstaller || (strcmp(argv[i], "--install") == 0);
forceDLCInstaller = forceDLCInstaller || (strcmp(argv[i], "--install-dlc") == 0);
useDefaultWorkingDirectory = useDefaultWorkingDirectory || (strcmp(argv[i], "--use-cwd") == 0);
forceInstallationCheck = forceInstallationCheck || (strcmp(argv[i], "--install-check") == 0);
if (strcmp(argv[i], "--sdl-video-driver") == 0)
{
@ -217,7 +226,70 @@ int main(int argc, char *argv[])
}
}
if (!useDefaultWorkingDirectory)
{
// Set the current working directory to the executable's path.
std::error_code ec;
std::filesystem::current_path(os::process::GetExecutablePath().parent_path(), ec);
}
Config::Load();
if (!PersistentStorageManager::LoadBinary())
LOGFN_ERROR("Failed to load persistent storage binary... (status code {})", (int)PersistentStorageManager::BinStatus);
if (forceInstallationCheck)
{
// Create the console to show progress to the user, otherwise it will seem as if the game didn't boot at all.
os::process::ShowConsole();
Journal journal;
double lastProgressMiB = 0.0;
double lastTotalMib = 0.0;
Installer::checkInstallIntegrity(GAME_INSTALL_DIRECTORY, journal, [&]()
{
constexpr double MiBDivisor = 1024.0 * 1024.0;
constexpr double MiBProgressThreshold = 128.0;
double progressMiB = double(journal.progressCounter) / MiBDivisor;
double totalMiB = double(journal.progressTotal) / MiBDivisor;
if (journal.progressCounter > 0)
{
if ((progressMiB - lastProgressMiB) > MiBProgressThreshold)
{
fprintf(stdout, "Checking files: %0.2f MiB / %0.2f MiB\n", progressMiB, totalMiB);
lastProgressMiB = progressMiB;
}
}
else
{
if ((totalMiB - lastTotalMib) > MiBProgressThreshold)
{
fprintf(stdout, "Scanning files: %0.2f MiB\n", totalMiB);
lastTotalMib = totalMiB;
}
}
return true;
});
char resultText[512];
uint32_t messageBoxStyle;
if (journal.lastResult == Journal::Result::Success)
{
snprintf(resultText, sizeof(resultText), "%s", Localise("IntegrityCheck_Success").c_str());
fprintf(stdout, "%s\n", resultText);
messageBoxStyle = SDL_MESSAGEBOX_INFORMATION;
}
else
{
snprintf(resultText, sizeof(resultText), Localise("IntegrityCheck_Failed").c_str(), journal.lastErrorMessage.c_str());
fprintf(stderr, "%s\n", resultText);
messageBoxStyle = SDL_MESSAGEBOX_ERROR;
}
SDL_ShowSimpleMessageBox(messageBoxStyle, GameWindow::GetTitle(), resultText, GameWindow::s_pWindow);
std::_Exit(int(journal.lastResult));
}
#if defined(_WIN32) && defined(UNLEASHED_RECOMP_D3D12)
for (auto& dll : g_D3D12RequiredModules)

View file

@ -191,7 +191,10 @@ void ModLoader::Init()
{
std::string includeDirU8 = modIni.getString("Main", fmt::format("IncludeDir{}", j), "");
if (!includeDirU8.empty())
{
std::replace(includeDirU8.begin(), includeDirU8.end(), '\\', '/');
mod.includeDirs.emplace_back(modDirectoryPath / std::u8string_view((const char8_t*)includeDirU8.c_str()));
}
}
if (!foundModSaveFilePath)

View file

@ -2,6 +2,7 @@
#include <api/SWA.h>
#include <install/update_checker.h>
#include <locale/locale.h>
#include <os/logger.h>
#include <ui/fader.h>
#include <ui/message_window.h>
#include <user/achievement_manager.h>
@ -64,16 +65,16 @@ static bool ProcessCorruptAchievementsMessage()
if (!g_corruptAchievementsMessageOpen)
return false;
auto message = AchievementManager::Status == EAchStatus::IOError
auto message = AchievementManager::BinStatus == EAchBinStatus::IOError
? Localise("Title_Message_AchievementDataIOError")
: Localise("Title_Message_AchievementDataCorrupt");
if (MessageWindow::Open(message, &g_corruptAchievementsMessageResult) == MSG_CLOSED)
{
// Allow user to proceed if the achievement data couldn't be loaded.
// Restarting may fix this error, so it isn't worth clearing the data for.
if (AchievementManager::Status != EAchStatus::IOError)
AchievementManager::Save(true);
// Create a new save file if the file was successfully loaded and failed validation.
// If the file couldn't be opened, restarting may fix this error, so it isn't worth clearing the data for.
if (AchievementManager::BinStatus != EAchBinStatus::IOError)
AchievementManager::SaveBinary(true);
g_corruptAchievementsMessageOpen = false;
g_corruptAchievementsMessageOpen.notify_one();
@ -135,9 +136,10 @@ void PressStartSaveLoadThreadMidAsmHook()
g_faderBegun.wait(true);
}
AchievementManager::Load();
if (!AchievementManager::LoadBinary())
LOGFN_ERROR("Failed to load achievement data... (status code {})", (int)AchievementManager::BinStatus);
if (AchievementManager::Status != EAchStatus::Success)
if (AchievementManager::BinStatus != EAchBinStatus::Success)
{
g_corruptAchievementsMessageOpen = true;
g_corruptAchievementsMessageOpen.wait(true);

View file

@ -711,8 +711,6 @@ static const xxHashMap<CsdModifier> g_modifiers =
// ui_shop
{ HashStr("ui_shop/footer/shop_footer"), { ALIGN_BOTTOM } },
{ HashStr("ui_shop/header/ring"), { ALIGN_TOP } },
{ HashStr("ui_shop/header/shop_nametag"), { ALIGN_TOP } },
// ui_start
{ HashStr("ui_start/Clear/position/bg/bg_1"), { STRETCH } },

View file

@ -1,6 +1,7 @@
#include <api/SWA.h>
#include <ui/game_window.h>
#include <user/achievement_manager.h>
#include <user/persistent_storage_manager.h>
#include <user/config.h>
void AchievementManagerUnlockMidAsmHook(PPCRegister& id)
@ -53,6 +54,16 @@ void WerehogBattleMusicMidAsmHook(PPCRegister& r11)
r11.u8 = 3;
}
bool UseAlternateTitleMidAsmHook()
{
auto isSWA = Config::Language == ELanguage::Japanese;
if (Config::UseAlternateTitle)
isSWA = !isSWA;
return isSWA;
}
/* Hook function that gets the game region
and force result to zero for Japanese
to display the correct logos. */
@ -147,12 +158,38 @@ PPC_FUNC(sub_824C1E60)
__imp__sub_824C1E60(ctx, base);
}
// Remove boost filter
void DisableBoostFilterMidAsmHook(PPCRegister& r11)
// This function is called in various places but primarily for the boost filter
// when the second argument (r4) is set to "boost". Whilst boosting the third argument (f1)
// will go up to 1.0f and then down to 0.0f as the player lets off of the boost button.
// To avoid the boost filter from kicking in at all if the function is called with "boost"
// we set the third argument to zero no matter what (if the code is on).
PPC_FUNC_IMPL(__imp__sub_82B4DB48);
PPC_FUNC(sub_82B4DB48)
{
if (Config::DisableBoostFilter)
if (Config::DisableBoostFilter && strcmp((const char*)(base + ctx.r4.u32), "boost") == 0)
{
if (r11.u32 == 1)
r11.u32 = 0;
ctx.f1.f64 = 0.0;
}
__imp__sub_82B4DB48(ctx, base);
}
// DLC save data flag check.
//
// The DLC checks are fundamentally broken in this game, resulting in this method always
// returning true and displaying the DLC info message when it shouldn't be.
//
// The original intent here seems to have been to display the message every time new DLC
// content is installed, but the flags in the save data never get written to properly,
// causing this function to always pass in some way.
//
// We bypass the save data completely and write to external persistent storage to store
// whether we've seen the DLC info message instead. This way we can retain the original
// broken game behaviour, whilst also providing a fix for this issue that is safe.
PPC_FUNC_IMPL(__imp__sub_824EE620);
PPC_FUNC(sub_824EE620)
{
__imp__sub_824EE620(ctx, base);
ctx.r3.u32 = PersistentStorageManager::ShouldDisplayDLCMessage(true);
}

View file

@ -2,11 +2,10 @@
#include <hid/hid.h>
#include <os/logger.h>
#include <user/achievement_manager.h>
#include <user/persistent_storage_manager.h>
#include <user/config.h>
#include <app.h>
bool m_isSavedAchievementData = false;
// SWA::Message::MsgRequestStartLoading::Impl
PPC_FUNC_IMPL(__imp__sub_824DCF38);
PPC_FUNC(sub_824DCF38)
@ -99,20 +98,23 @@ PPC_FUNC(sub_824E5170)
App::s_isSaving = pSaveIcon->m_IsVisible;
static bool isSavedExtraData = false;
if (pSaveIcon->m_IsVisible)
{
App::s_isSaveDataCorrupt = false;
if (!m_isSavedAchievementData)
if (!isSavedExtraData)
{
AchievementManager::Save();
AchievementManager::SaveBinary();
PersistentStorageManager::SaveBinary();
m_isSavedAchievementData = true;
isSavedExtraData = true;
}
}
else
{
m_isSavedAchievementData = false;
isSavedExtraData = false;
}
}

View file

@ -0,0 +1,104 @@
#include "preload_executable.h"
#include <os/logger.h>
// Code from Zelda 64: Recompiled
// https://github.com/Zelda64Recomp/Zelda64Recomp/blob/91db87632c2bfb6995ef1554ec71b11977c621f8/src/main/main.cpp#L440-L514
PreloadContext::~PreloadContext()
{
#ifdef _WIN32
if (preloaded)
{
VirtualUnlock(view, size);
CloseHandle(mappingHandle);
CloseHandle(handle);
}
#endif
}
void PreloadContext::PreloadExecutable()
{
#ifdef _WIN32
wchar_t moduleName[MAX_PATH];
GetModuleFileNameW(NULL, moduleName, MAX_PATH);
handle = CreateFileW(moduleName, GENERIC_READ, FILE_SHARE_READ, nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (handle == INVALID_HANDLE_VALUE)
{
LOG_ERROR("Failed to load executable into memory!");
*this = {};
return;
}
LARGE_INTEGER moduleSize;
if (!GetFileSizeEx(handle, &moduleSize))
{
LOG_ERROR("Failed to get size of executable!");
CloseHandle(handle);
*this = {};
return;
}
size = moduleSize.QuadPart;
mappingHandle = CreateFileMappingW(handle, nullptr, PAGE_READONLY, 0, 0, nullptr);
if (mappingHandle == nullptr)
{
LOG_ERROR("Failed to create file mapping of executable!");
CloseHandle(handle);
*this = {};
return;
}
view = MapViewOfFile(mappingHandle, FILE_MAP_READ, 0, 0, 0);
if (view == nullptr)
{
LOG_ERROR("Failed to map view of of executable!");
CloseHandle(mappingHandle);
CloseHandle(handle);
*this = {};
return;
}
DWORD pid = GetCurrentProcessId();
HANDLE processHandle = OpenProcess(PROCESS_SET_QUOTA | PROCESS_QUERY_INFORMATION, FALSE, pid);
if (processHandle == nullptr)
{
LOG_ERROR("Failed to open own process!");
CloseHandle(mappingHandle);
CloseHandle(handle);
*this = {};
return;
}
SIZE_T minimumSetSize, maximumSetSize;
if (!GetProcessWorkingSetSize(processHandle, &minimumSetSize, &maximumSetSize))
{
LOG_ERROR("Failed to get working set size!");
CloseHandle(mappingHandle);
CloseHandle(handle);
*this = {};
return;
}
if (!SetProcessWorkingSetSize(processHandle, minimumSetSize + size, maximumSetSize + size))
{
LOG_ERROR("Failed to set working set size!");
CloseHandle(mappingHandle);
CloseHandle(handle);
*this = {};
return;
}
if (VirtualLock(view, size) == 0)
{
LOGF_ERROR("Failed to lock view of executable! (Error: 0x{:X})\n", GetLastError());
CloseHandle(mappingHandle);
CloseHandle(handle);
*this = {};
return;
}
preloaded = true;
#endif
}

View file

@ -0,0 +1,15 @@
#pragma once
struct PreloadContext
{
#ifdef _WIN32
HANDLE handle{};
HANDLE mappingHandle{};
SIZE_T size{};
PVOID view{};
bool preloaded{};
#endif
~PreloadContext();
void PreloadExecutable();
};

View file

@ -282,7 +282,12 @@ const char* GameWindow::GetTitle()
{
if (Config::UseOfficialTitleOnTitleBar)
{
return Config::Language == ELanguage::Japanese
auto isSWA = Config::Language == ELanguage::Japanese;
if (Config::UseAlternateTitle)
isSWA = !isSWA;
return isSWA
? "SONIC WORLD ADVENTURE"
: "SONIC UNLEASHED";
}

View file

@ -80,6 +80,7 @@ static double g_lockedOnTime;
static double g_lastTappedTime;
static double g_lastIncrementTime;
static double g_lastIncrementSoundTime;
static double g_fastIncrementHoldTime;
static constexpr size_t GRID_SIZE = 9;
@ -98,6 +99,7 @@ static bool g_isEnterKeyBuffered = false;
static bool g_canReset = false;
static bool g_isLanguageOptionChanged = false;
static bool g_titleAnimBegin = true;
static EChannelConfiguration g_currentChannelConfig;
static double g_appearTime = 0.0;
@ -802,7 +804,6 @@ static void DrawConfigOption(int32_t rowIndex, float yOffset, ConfigDef<T>* conf
config->Callback(config);
VideoConfigValueChangedCallback(config);
XAudioConfigValueChangedCallback(config);
Game_PlaySound("sys_worldmap_finaldecide");
}
@ -835,7 +836,6 @@ static void DrawConfigOption(int32_t rowIndex, float yOffset, ConfigDef<T>* conf
if (config->Value != s_oldValue)
{
VideoConfigValueChangedCallback(config);
XAudioConfigValueChangedCallback(config);
if (config->ApplyCallback)
config->ApplyCallback(config);
@ -864,7 +864,6 @@ static void DrawConfigOption(int32_t rowIndex, float yOffset, ConfigDef<T>* conf
config->MakeDefault();
VideoConfigValueChangedCallback(config);
XAudioConfigValueChangedCallback(config);
if (config->Callback)
config->Callback(config);
@ -1043,37 +1042,40 @@ static void DrawConfigOption(int32_t rowIndex, float yOffset, ConfigDef<T>* conf
}
config->Value = it->first;
config->SnapToNearestAccessibleValue(rightTapped);
if (increment || decrement)
Game_PlaySound("sys_actstg_pausecursor");
}
else if constexpr (std::is_same_v<T, float> || std::is_same_v<T, int32_t>)
{
float deltaTime = ImGui::GetIO().DeltaTime;
float deltaTime = std::fmin(ImGui::GetIO().DeltaTime, 1.0f / 15.0f);
bool fastIncrement = (time - g_lastTappedTime) > 0.5;
bool fastIncrement = isSlider && (leftIsHeld || rightIsHeld) && (time - g_lastTappedTime) > 0.5;
bool isPlayIncrementSound = true;
constexpr double INCREMENT_TIME = 1.0 / 120.0;
constexpr double INCREMENT_SOUND_TIME = 1.0 / 7.5;
if (isSlider)
if (fastIncrement)
g_fastIncrementHoldTime += deltaTime;
else
g_fastIncrementHoldTime = 0;
if (fastIncrement)
{
if (fastIncrement)
{
isPlayIncrementSound = (time - g_lastIncrementSoundTime) > INCREMENT_SOUND_TIME;
isPlayIncrementSound = (time - g_lastIncrementSoundTime) > INCREMENT_SOUND_TIME;
if ((time - g_lastIncrementTime) < INCREMENT_TIME)
fastIncrement = false;
else
g_lastIncrementTime = time;
}
if (g_fastIncrementHoldTime < INCREMENT_TIME)
fastIncrement = false;
else
g_lastIncrementTime = time;
}
if (fastIncrement)
{
decrement = leftIsHeld;
increment = rightIsHeld;
}
if (fastIncrement)
{
decrement = leftIsHeld;
increment = rightIsHeld;
}
do
@ -1093,9 +1095,10 @@ static void DrawConfigOption(int32_t rowIndex, float yOffset, ConfigDef<T>* conf
config->Value += 0.01f;
}
deltaTime -= INCREMENT_TIME;
if (fastIncrement)
g_fastIncrementHoldTime -= INCREMENT_TIME;
}
while (fastIncrement && deltaTime > 0.0f);
while (fastIncrement && g_fastIncrementHoldTime >= INCREMENT_TIME);
bool isConfigValueInBounds = config->Value >= valueMin && config->Value <= valueMax;
@ -1245,7 +1248,7 @@ static void DrawConfigOptions()
DrawConfigOption(rowCount++, yOffset, &Config::MasterVolume, true);
DrawConfigOption(rowCount++, yOffset, &Config::MusicVolume, true);
DrawConfigOption(rowCount++, yOffset, &Config::EffectsVolume, true);
DrawConfigOption(rowCount++, yOffset, &Config::ChannelConfiguration, true);
DrawConfigOption(rowCount++, yOffset, &Config::ChannelConfiguration, !OptionsMenu::s_isPause, cmnReason);
DrawConfigOption(rowCount++, yOffset, &Config::MusicAttenuation, AudioPatches::CanAttenuate(), &Localise("Options_Desc_OSNotSupported"));
DrawConfigOption(rowCount++, yOffset, &Config::BattleTheme, true);
break;
@ -1271,7 +1274,7 @@ static void DrawConfigOptions()
DrawConfigOption(rowCount++, yOffset, &Config::VSync, true);
DrawConfigOption(rowCount++, yOffset, &Config::FPS, true, nullptr, FPS_MIN, 120, FPS_MAX);
DrawConfigOption(rowCount++, yOffset, &Config::Brightness, true);
DrawConfigOption(rowCount++, yOffset, &Config::AntiAliasing, true);
DrawConfigOption(rowCount++, yOffset, &Config::AntiAliasing, Config::AntiAliasing.InaccessibleValues.size() != 3, &Localise("Options_Desc_NotAvailableHardware"));
DrawConfigOption(rowCount++, yOffset, &Config::TransparencyAntiAliasing, Config::AntiAliasing != EAntiAliasing::None, &Localise("Options_Desc_NotAvailableMSAA"));
DrawConfigOption(rowCount++, yOffset, &Config::ShadowResolution, true);
DrawConfigOption(rowCount++, yOffset, &Config::GITextureFiltering, true);
@ -1786,7 +1789,7 @@ void OptionsMenu::Draw()
DrawFadeTransition();
}
s_isRestartRequired = Config::Language != App::s_language;
s_isRestartRequired = Config::Language != App::s_language || Config::ChannelConfiguration != g_currentChannelConfig;
}
void OptionsMenu::Open(bool isPause, SWA::EMenuType pauseMenuType)
@ -1802,6 +1805,7 @@ void OptionsMenu::Open(bool isPause, SWA::EMenuType pauseMenuType)
g_categoryAnimMax = { 0.0f, 0.0f };
g_selectedItem = nullptr;
g_titleAnimBegin = true;
g_currentChannelConfig = Config::ChannelConfiguration;
/* Store button state so we can track it later
and prevent the first item being selected. */

View file

@ -11,7 +11,7 @@ bool AchievementData::VerifySignature() const
bool AchievementData::VerifyVersion() const
{
return Version == AchVersion ACH_VERSION;
return Version <= ACH_VERSION;
}
bool AchievementData::VerifyChecksum()

View file

@ -4,27 +4,12 @@
#define ACH_FILENAME "ACH-DATA"
#define ACH_SIGNATURE { 'A', 'C', 'H', ' ' }
#define ACH_VERSION { 1, 0, 0 }
#define ACH_VERSION 1
#define ACH_RECORDS 50
class AchievementData
{
public:
struct AchVersion
{
uint8_t Major;
uint8_t Minor;
uint8_t Revision;
uint8_t Reserved;
bool operator==(const AchVersion& other) const
{
return Major == other.Major &&
Minor == other.Minor &&
Revision == other.Revision;
}
};
#pragma pack(push, 1)
struct AchRecord
{
@ -35,10 +20,10 @@ public:
#pragma pack(pop)
char Signature[4] ACH_SIGNATURE;
AchVersion Version ACH_VERSION;
uint32_t Checksum;
uint32_t Reserved;
AchRecord Records[ACH_RECORDS];
uint32_t Version{ ACH_VERSION };
uint32_t Checksum{};
uint32_t Reserved{};
AchRecord Records[ACH_RECORDS]{};
bool VerifySignature() const;
bool VerifyVersion() const;

View file

@ -1,5 +1,6 @@
#include "achievement_manager.h"
#include <os/logger.h>
#include <kernel/memory.h>
#include <ui/achievement_overlay.h>
#include <user/config.h>
@ -84,13 +85,31 @@ void AchievementManager::UnlockAll()
void AchievementManager::Reset()
{
Data = {};
// The first usage of the shoe upgrades get stored within a session persistent boolean flag.
// This causes issues with popping the achievement for the use of these abilities when the player
// starts a new save file after they already used them in a session as these bools are never reset
// unless the game is exited.
// As a solution we reset these flags whenever the achievement data is being reset too.
// Lay the Smackdown
*(bool*)g_memory.Translate(0x833647C5) = false;
// Wall Crawler
*(bool*)g_memory.Translate(0x83363004) = false;
// Airdevil
*(bool*)g_memory.Translate(0x833647BC) = false;
// Hyperdrive
*(bool*)g_memory.Translate(0x833647C4) = false;
}
void AchievementManager::Load()
bool AchievementManager::LoadBinary()
{
AchievementManager::Reset();
Status = EAchStatus::Success;
BinStatus = EAchBinStatus::Success;
auto dataPath = GetDataPath(true);
@ -100,7 +119,7 @@ void AchievementManager::Load()
dataPath = GetDataPath(false);
if (!std::filesystem::exists(dataPath))
return;
return true;
}
std::error_code ec;
@ -109,16 +128,16 @@ void AchievementManager::Load()
if (fileSize != dataSize)
{
Status = EAchStatus::BadFileSize;
return;
BinStatus = EAchBinStatus::BadFileSize;
return false;
}
std::ifstream file(dataPath, std::ios::binary);
if (!file)
{
Status = EAchStatus::IOError;
return;
BinStatus = EAchBinStatus::IOError;
return false;
}
AchievementData data{};
@ -127,19 +146,18 @@ void AchievementManager::Load()
if (!data.VerifySignature())
{
Status = EAchStatus::BadSignature;
BinStatus = EAchBinStatus::BadSignature;
file.close();
return;
return false;
}
file.read((char*)&data.Version, sizeof(data.Version));
// TODO: upgrade in future if the version changes.
if (!data.VerifyVersion())
{
Status = EAchStatus::BadVersion;
BinStatus = EAchBinStatus::BadVersion;
file.close();
return;
return false;
}
file.seekg(0);
@ -147,22 +165,24 @@ void AchievementManager::Load()
if (!data.VerifyChecksum())
{
Status = EAchStatus::BadChecksum;
BinStatus = EAchBinStatus::BadChecksum;
file.close();
return;
return false;
}
file.close();
memcpy(&Data, &data, dataSize);
return true;
}
void AchievementManager::Save(bool ignoreStatus)
bool AchievementManager::SaveBinary(bool ignoreStatus)
{
if (!ignoreStatus && Status != EAchStatus::Success)
if (!ignoreStatus && BinStatus != EAchBinStatus::Success)
{
LOGN_WARNING("Achievement data will not be saved in this session!");
return;
return false;
}
LOGN("Saving achievements...");
@ -172,7 +192,7 @@ void AchievementManager::Save(bool ignoreStatus)
if (!file)
{
LOGN_ERROR("Failed to write achievement data.");
return;
return false;
}
Data.Checksum = Data.CalculateChecksum();
@ -180,5 +200,7 @@ void AchievementManager::Save(bool ignoreStatus)
file.write((const char*)&Data, sizeof(AchievementData));
file.close();
Status = EAchStatus::Success;
BinStatus = EAchBinStatus::Success;
return true;
}

View file

@ -2,7 +2,7 @@
#include <user/achievement_data.h>
enum class EAchStatus
enum class EAchBinStatus
{
Success,
IOError,
@ -16,7 +16,7 @@ class AchievementManager
{
public:
static inline AchievementData Data{};
static inline EAchStatus Status{};
static inline EAchBinStatus BinStatus{ EAchBinStatus::Success };
static std::filesystem::path GetDataPath(bool checkForMods)
{
@ -29,6 +29,6 @@ public:
static void Unlock(uint16_t id);
static void UnlockAll();
static void Reset();
static void Load();
static void Save(bool ignoreStatus = false);
static bool LoadBinary();
static bool SaveBinary(bool ignoreStatus = false);
};

View file

@ -494,6 +494,9 @@ template<typename T, bool isHidden>
void ConfigDef<T, isHidden>::MakeDefault()
{
Value = DefaultValue;
if constexpr (std::is_enum_v<T>)
SnapToNearestAccessibleValue(false);
}
template<typename T, bool isHidden>
@ -696,6 +699,51 @@ void ConfigDef<T, isHidden>::GetLocaleStrings(std::vector<std::string_view>& loc
}
}
template<typename T, bool isHidden>
void ConfigDef<T, isHidden>::SnapToNearestAccessibleValue(bool searchUp)
{
if constexpr (std::is_enum_v<T>)
{
if (EnumTemplateReverse.empty() || InaccessibleValues.empty())
return;
if (EnumTemplateReverse.size() == InaccessibleValues.size())
{
assert(false && "All enum values are marked inaccessible and the nearest accessible value cannot be determined.");
return;
}
auto it = EnumTemplateReverse.find(Value);
if (it == EnumTemplateReverse.end())
{
assert(false && "Enum value does not exist in the template.");
return;
}
// Skip the enum value if it's marked as inaccessible.
while (InaccessibleValues.find(it->first) != InaccessibleValues.end())
{
if (searchUp)
{
++it;
if (it == EnumTemplateReverse.end())
it = EnumTemplateReverse.begin();
}
else
{
if (it == EnumTemplateReverse.begin())
it = EnumTemplateReverse.end();
--it;
}
}
Value = it->first;
}
}
std::filesystem::path Config::GetConfigPath()
{
return GetUserPath() / "config.toml";

View file

@ -20,6 +20,7 @@ public:
virtual std::string GetDefinition(bool withSection = false) const = 0;
virtual std::string ToString(bool strWithQuotes = true) const = 0;
virtual void GetLocaleStrings(std::vector<std::string_view>& localeStrings) const = 0;
virtual void SnapToNearestAccessibleValue(bool searchUp) = 0;
};
#define CONFIG_LOCALE std::unordered_map<ELanguage, std::tuple<std::string, std::string>>
@ -158,7 +159,8 @@ public:
CONFIG_LOCALE* Locale{};
T DefaultValue{};
T Value{ DefaultValue };
std::unordered_map<std::string, T>* EnumTemplate;
std::set<T> InaccessibleValues{};
std::unordered_map<std::string, T>* EnumTemplate{};
std::map<T, std::string> EnumTemplateReverse{};
CONFIG_ENUM_LOCALE(T)* EnumLocale{};
std::function<void(ConfigDef<T, isHidden>*)> Callback;
@ -183,25 +185,20 @@ public:
~ConfigDef();
bool IsHidden() override;
void ReadValue(toml::v3::ex::parse_result& toml) override;
void MakeDefault() override;
std::string_view GetSection() const override;
std::string_view GetName() const override;
std::string GetNameLocalised(ELanguage language) const override;
std::string GetDescription(ELanguage language) const override;
bool IsDefaultValue() const override;
const void* GetValue() const override;
std::string GetValueLocalised(ELanguage language) const override;
std::string GetValueDescription(ELanguage language) const override;
std::string GetDefinition(bool withSection = false) const override;
std::string ToString(bool strWithQuotes = true) const override;
void GetLocaleStrings(std::vector<std::string_view>& localeStrings) const override;
void SnapToNearestAccessibleValue(bool searchUp) override;
operator T() const
{

View file

@ -92,6 +92,7 @@ CONFIG_DEFINE_HIDDEN("Codes", bool, HomingAttackOnJump, false);
CONFIG_DEFINE_HIDDEN("Codes", bool, HUDToggleKey, false);
CONFIG_DEFINE_HIDDEN("Codes", bool, SaveScoreAtCheckpoints, false);
CONFIG_DEFINE_HIDDEN("Codes", bool, SkipIntroLogos, false);
CONFIG_DEFINE_HIDDEN("Codes", bool, UseAlternateTitle, false);
CONFIG_DEFINE_HIDDEN("Codes", bool, UseArrowsForTimeOfDayTransition, false);
CONFIG_DEFINE_HIDDEN("Codes", bool, UseOfficialTitleOnTitleBar, false);

View file

@ -0,0 +1,13 @@
#include "persistent_data.h"
bool PersistentData::VerifySignature() const
{
char sig[4] = EXT_SIGNATURE;
return memcmp(Signature, sig, sizeof(Signature)) == 0;
}
bool PersistentData::VerifyVersion() const
{
return Version <= EXT_VERSION;
}

View file

@ -0,0 +1,30 @@
#pragma once
#include <user/paths.h>
#define EXT_FILENAME "EXT-DATA"
#define EXT_SIGNATURE { 'E', 'X', 'T', ' ' }
#define EXT_VERSION 1
enum class EDLCFlag
{
ApotosAndShamar,
Spagonia,
Chunnan,
Mazuri,
Holoska,
EmpireCityAndAdabat,
Count
};
class PersistentData
{
public:
char Signature[4] EXT_SIGNATURE;
uint32_t Version{ EXT_VERSION };
uint64_t Reserved{};
bool DLCFlags[6]{};
bool VerifySignature() const;
bool VerifyVersion() const;
};

View file

@ -0,0 +1,117 @@
#include "persistent_storage_manager.h"
#include <install/installer.h>
#include <os/logger.h>
#include <user/paths.h>
bool PersistentStorageManager::ShouldDisplayDLCMessage(bool setOffendingDLCFlag)
{
if (BinStatus != EExtBinStatus::Success)
return true;
static std::unordered_map<EDLCFlag, DLC> flags =
{
{ EDLCFlag::ApotosAndShamar, DLC::ApotosShamar },
{ EDLCFlag::Spagonia, DLC::Spagonia },
{ EDLCFlag::Chunnan, DLC::Chunnan },
{ EDLCFlag::Mazuri, DLC::Mazuri },
{ EDLCFlag::Holoska, DLC::Holoska },
{ EDLCFlag::EmpireCityAndAdabat, DLC::EmpireCityAdabat }
};
auto result = false;
for (auto& pair : flags)
{
if (!Data.DLCFlags[(int)pair.first] && Installer::checkDLCInstall(GetGamePath(), pair.second))
{
if (setOffendingDLCFlag)
Data.DLCFlags[(int)pair.first] = true;
result = true;
}
}
return result;
}
bool PersistentStorageManager::LoadBinary()
{
BinStatus = EExtBinStatus::Success;
auto dataPath = GetDataPath(true);
if (!std::filesystem::exists(dataPath))
{
// Try loading base persistent data as fallback.
dataPath = GetDataPath(false);
if (!std::filesystem::exists(dataPath))
return true;
}
std::error_code ec;
auto fileSize = std::filesystem::file_size(dataPath, ec);
auto dataSize = sizeof(PersistentData);
if (fileSize != dataSize)
{
BinStatus = EExtBinStatus::BadFileSize;
return false;
}
std::ifstream file(dataPath, std::ios::binary);
if (!file)
{
BinStatus = EExtBinStatus::IOError;
return false;
}
PersistentData data{};
file.read((char*)&data.Signature, sizeof(data.Signature));
if (!data.VerifySignature())
{
BinStatus = EExtBinStatus::BadSignature;
file.close();
return false;
}
file.read((char*)&data.Version, sizeof(data.Version));
if (!data.VerifyVersion())
{
BinStatus = EExtBinStatus::BadVersion;
file.close();
return false;
}
file.seekg(0);
file.read((char*)&data, sizeof(data));
file.close();
memcpy(&Data, &data, dataSize);
return true;
}
bool PersistentStorageManager::SaveBinary()
{
LOGN("Saving persistent storage binary...");
std::ofstream file(GetDataPath(true), std::ios::binary);
if (!file)
{
LOGN_ERROR("Failed to write persistent storage binary.");
return false;
}
file.write((const char*)&Data, sizeof(PersistentData));
file.close();
BinStatus = EExtBinStatus::Success;
return true;
}

View file

@ -0,0 +1,28 @@
#pragma once
#include <user/persistent_data.h>
enum class EExtBinStatus
{
Success,
IOError,
BadFileSize,
BadSignature,
BadVersion
};
class PersistentStorageManager
{
public:
static inline PersistentData Data{};
static inline EExtBinStatus BinStatus{ EExtBinStatus::Success };
static std::filesystem::path GetDataPath(bool checkForMods)
{
return GetSavePath(checkForMods) / EXT_FILENAME;
}
static bool ShouldDisplayDLCMessage(bool setOffendingDLCFlag);
static bool LoadBinary();
static bool SaveBinary();
};

View file

@ -1102,6 +1102,7 @@ address = 0x82614948
registers = ["r3"]
[[midasm_hook]]
name = "DisableBoostFilterMidAsmHook"
address = 0x82B48C9C
registers = ["r11"]
name = "UseAlternateTitleMidAsmHook"
address = 0x82580F44
jump_address_on_true = 0x82580F48
jump_address_on_false = 0x82580FA0

@ -1 +1 @@
Subproject commit e5a4adccb30734321ac17347090abeb6690dab70
Subproject commit 35322388006365a648f75f4981a496b12a7f7478

2
thirdparty/ddspp vendored

@ -1 +1 @@
Subproject commit 1390499ec9f7b82e7a9cbdeb2e6191808e981f84
Subproject commit 98ce1d384706c8d7121876742a786f4eb89a23ef

@ -1 +1 @@
Subproject commit 855a5a8c51ea5f84baecbf4fc87c182795d482c9
Subproject commit 4897cf7ef2070120310c28a1a672b427d745dad8