Icon culling hides car icons when zoomed out past a configurable threshold (default 40%), keeping only locomotive icons visible. Locos scale up automatically when culling is active (configurable boost). An option also hides MU trailing units. All managed by the new MapIconCuller class with smooth fade transitions. EOTD (End-of-Train Device): a blinking red dot at the rear of each consist. Shown by default when car icons are hidden. Dot size and visibility conditions are configurable. Implemented in EotdSystem as a programmatically generated circle texture to avoid asset dependencies. Added an Icons submenu to the gear menu containing all icon and EOTD settings. Controls grey out to reflect dependencies: culling sub-settings (MU hide, loco boost, EOTD) grey out when culling is off; EOTD dot size and hide-when-visible grey out when EOTD is off. Changes round-trip via UICmd events, persist to PopoutSettings, and call MapIconCuller.Refresh(). Native state is seeded from C# on window init via RRPOPOUT_SetIconCullingState. Settings completeness: added Right-click recenters toggle and Map BG opacity slider to the UMM settings panel (were only accessible from the gear menu). Replaced four FindObjectOfType<MapWindow> calls with PanelFinder.GetMapWindow(). Fixed AV false-positive on release zips: ImGui's built-in font blob contained the substring "UPX", matching the heuristic for UPX-packed binaries. Added IMGUI_DISABLE_DEFAULT_FONT to strip the blob; ProggyClean.ttf is now shipped as a separate file alongside the DLL and loaded at runtime instead.
1331 lines
62 KiB
C++
1331 lines
62 KiB
C++
#define WIN32_LEAN_AND_MEAN
|
|
#define NOMINMAX
|
|
#include <Windows.h>
|
|
#include <d3d11.h>
|
|
#include <dxgi.h>
|
|
#include <d3dcompiler.h>
|
|
#include <wrl/client.h>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <mutex>
|
|
#include "d3d11_renderer.h"
|
|
#include "popout_window.h"
|
|
#include "popout_windows.h"
|
|
#include "../include/shared_types.h"
|
|
|
|
// Dear ImGui + D3D11 backend
|
|
#include "imgui.h"
|
|
#include "imgui_impl_dx11.h"
|
|
|
|
using Microsoft::WRL::ComPtr;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Shared device/context captured from Unity via UnityPluginLoad
|
|
// ---------------------------------------------------------------------------
|
|
static ID3D11Device* g_device = nullptr;
|
|
static ID3D11DeviceContext* g_context = nullptr;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Shared blit resources (created once in Renderer_OnDeviceInit)
|
|
// ---------------------------------------------------------------------------
|
|
static ComPtr<ID3D11VertexShader> g_vs;
|
|
static ComPtr<ID3D11PixelShader> g_ps;
|
|
static ComPtr<ID3D11Buffer> g_uvRectCB;
|
|
static ComPtr<ID3D11SamplerState> g_sampler;
|
|
static ComPtr<ID3D11BlendState> g_blendOpaque;
|
|
static ComPtr<ID3D11RasterizerState> g_rasterState;
|
|
static ComPtr<ID3D11DepthStencilState> g_depthState;
|
|
|
|
static bool g_imguiInited = false;
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Theme state — written by Renderer_SetTheme (main thread), applied on the
|
|
// render thread at frame start. Mutex protects the struct copy; g_themeChanged
|
|
// signals the render thread to call ApplyThemeToStyle before NewFrame.
|
|
// ---------------------------------------------------------------------------
|
|
static MapThemeData g_theme;
|
|
static std::mutex g_themeMutex;
|
|
static std::atomic<bool> g_themeChanged {false};
|
|
static std::atomic<int> g_currentThemePreset {0};
|
|
static std::atomic<float> g_ovAlpha {1.0f}; // chrome (window + toolbar + compass) alpha
|
|
static std::atomic<float> g_mapAlpha {1.0f}; // map Image() alpha, independent of chrome
|
|
static std::atomic<float> g_mapBgAlpha {1.0f}; // camera clear colour opacity
|
|
|
|
// cbuffer layout — must be 16-byte aligned
|
|
struct alignas(16) UVRectCB { float u0, v0, u1, v1; };
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// HLSL: fullscreen triangle blit
|
|
// ---------------------------------------------------------------------------
|
|
static const char* kBlitVS = R"hlsl(
|
|
cbuffer UVRect : register(b0) { float4 _uvRect; };
|
|
struct VSOut { float4 pos : SV_Position; float2 uv : TEXCOORD0; };
|
|
VSOut main(uint id : SV_VertexID) {
|
|
VSOut o;
|
|
float2 base = float2((id & 2u) ? 2.f : 0.f, (id & 1u) ? 2.f : 0.f);
|
|
o.pos = float4(base.x * 2.f - 1.f, 1.f - base.y * 2.f, 0.f, 1.f);
|
|
o.uv = lerp(_uvRect.xy, _uvRect.zw, base);
|
|
return o;
|
|
}
|
|
)hlsl";
|
|
|
|
static const char* kBlitPS = R"hlsl(
|
|
Texture2D gTex : register(t0);
|
|
SamplerState gSamp : register(s0);
|
|
struct VSOut { float4 pos : SV_Position; float2 uv : TEXCOORD0; };
|
|
float4 main(VSOut i) : SV_Target { return gTex.Sample(gSamp, i.uv); }
|
|
)hlsl";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
static DXGI_FORMAT TypedFormat(DXGI_FORMAT f) {
|
|
switch (f) {
|
|
case DXGI_FORMAT_R8G8B8A8_TYPELESS: return DXGI_FORMAT_R8G8B8A8_UNORM;
|
|
case DXGI_FORMAT_B8G8R8A8_TYPELESS: return DXGI_FORMAT_B8G8R8A8_UNORM;
|
|
case DXGI_FORMAT_R16G16B16A16_TYPELESS: return DXGI_FORMAT_R16G16B16A16_FLOAT;
|
|
case DXGI_FORMAT_R32G32B32A32_TYPELESS: return DXGI_FORMAT_R32G32B32A32_FLOAT;
|
|
case DXGI_FORMAT_R10G10B10A2_TYPELESS: return DXGI_FORMAT_R10G10B10A2_UNORM;
|
|
case DXGI_FORMAT_R32G32_TYPELESS: return DXGI_FORMAT_R32G32_FLOAT;
|
|
default: return f;
|
|
}
|
|
}
|
|
|
|
// Non-sRGB (linear) render-target view format for a backbuffer, so ImGui's writes
|
|
// are NOT sRGB-encoded by the GPU (which brightens). Returns UNKNOWN for formats
|
|
// where no such fix is needed/possible (e.g. float HDR has no encode anyway).
|
|
static DXGI_FORMAT UnormRtvFormat(DXGI_FORMAT f) {
|
|
switch (f) {
|
|
case DXGI_FORMAT_R8G8B8A8_TYPELESS:
|
|
case DXGI_FORMAT_R8G8B8A8_UNORM:
|
|
case DXGI_FORMAT_R8G8B8A8_UNORM_SRGB: return DXGI_FORMAT_R8G8B8A8_UNORM;
|
|
case DXGI_FORMAT_B8G8R8A8_TYPELESS:
|
|
case DXGI_FORMAT_B8G8R8A8_UNORM:
|
|
case DXGI_FORMAT_B8G8R8A8_UNORM_SRGB: return DXGI_FORMAT_B8G8R8A8_UNORM;
|
|
case DXGI_FORMAT_R10G10B10A2_TYPELESS:
|
|
case DXGI_FORMAT_R10G10B10A2_UNORM: return DXGI_FORMAT_R10G10B10A2_UNORM;
|
|
default: return DXGI_FORMAT_UNKNOWN;
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// D3D11 state save/restore — prevents corrupting Unity's render state
|
|
// ---------------------------------------------------------------------------
|
|
struct D3D11StateBlock {
|
|
ComPtr<ID3D11RenderTargetView> rtv[8];
|
|
ComPtr<ID3D11DepthStencilView> dsv;
|
|
UINT numViewports = D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE;
|
|
D3D11_VIEWPORT viewports[D3D11_VIEWPORT_AND_SCISSORRECT_OBJECT_COUNT_PER_PIPELINE];
|
|
ComPtr<ID3D11VertexShader> vs;
|
|
ComPtr<ID3D11PixelShader> ps;
|
|
ComPtr<ID3D11Buffer> vsCB;
|
|
ComPtr<ID3D11InputLayout> inputLayout;
|
|
D3D11_PRIMITIVE_TOPOLOGY topology = D3D11_PRIMITIVE_TOPOLOGY_UNDEFINED;
|
|
ComPtr<ID3D11ShaderResourceView> psSRV[1];
|
|
ComPtr<ID3D11SamplerState> psSampler[1];
|
|
ComPtr<ID3D11BlendState> blendState;
|
|
FLOAT blendFactor[4];
|
|
UINT sampleMask;
|
|
ComPtr<ID3D11RasterizerState> rasterizerState;
|
|
ComPtr<ID3D11DepthStencilState> depthStencilState;
|
|
UINT stencilRef;
|
|
|
|
void Capture(ID3D11DeviceContext* ctx) {
|
|
ctx->OMGetRenderTargets(8, &rtv[0], &dsv);
|
|
ctx->RSGetViewports(&numViewports, viewports);
|
|
ctx->VSGetShader(&vs, nullptr, nullptr);
|
|
ctx->VSGetConstantBuffers(0, 1, &vsCB);
|
|
ctx->PSGetShader(&ps, nullptr, nullptr);
|
|
ctx->IAGetInputLayout(&inputLayout);
|
|
ctx->IAGetPrimitiveTopology(&topology);
|
|
ctx->PSGetShaderResources(0, 1, &psSRV[0]);
|
|
ctx->PSGetSamplers(0, 1, &psSampler[0]);
|
|
ctx->OMGetBlendState(&blendState, blendFactor, &sampleMask);
|
|
ctx->RSGetState(&rasterizerState);
|
|
ctx->OMGetDepthStencilState(&depthStencilState, &stencilRef);
|
|
}
|
|
void Restore(ID3D11DeviceContext* ctx) {
|
|
ID3D11RenderTargetView* rtvPtrs[8];
|
|
for (int i = 0; i < 8; i++) rtvPtrs[i] = rtv[i].Get();
|
|
ctx->OMSetRenderTargets(8, rtvPtrs, dsv.Get());
|
|
ctx->RSSetViewports(numViewports, viewports);
|
|
ctx->VSSetShader(vs.Get(), nullptr, 0);
|
|
ID3D11Buffer* cb = vsCB.Get();
|
|
ctx->VSSetConstantBuffers(0, 1, &cb);
|
|
ctx->PSSetShader(ps.Get(), nullptr, 0);
|
|
ctx->IASetInputLayout(inputLayout.Get());
|
|
ctx->IASetPrimitiveTopology(topology);
|
|
ctx->PSSetShaderResources(0, 1, &psSRV[0]);
|
|
ctx->PSSetSamplers(0, 1, &psSampler[0]);
|
|
ctx->OMSetBlendState(blendState.Get(), blendFactor, sampleMask);
|
|
ctx->RSSetState(rasterizerState.Get());
|
|
ctx->OMSetDepthStencilState(depthStencilState.Get(), stencilRef);
|
|
}
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Theme helpers — render-thread only
|
|
// ---------------------------------------------------------------------------
|
|
|
|
// Apply g_theme to ImGui's style. Safe after ImGui::CreateContext(); does not need
|
|
// the DX11 backend. Called on the render thread before NewFrame, or from InitImGui.
|
|
static void ApplyThemeToStyle(const MapThemeData& t) {
|
|
ImGuiStyle& s = ImGui::GetStyle();
|
|
|
|
auto iv4 = [](float r, float g, float b, float a) { return ImVec4(r, g, b, a); };
|
|
auto darken = [](ImVec4 c, float f) {
|
|
return ImVec4(c.x*f, c.y*f, c.z*f, c.w);
|
|
};
|
|
auto brighten = [](ImVec4 c, float f) {
|
|
return ImVec4(std::min(1.f, c.x*f), std::min(1.f, c.y*f), std::min(1.f, c.z*f), c.w);
|
|
};
|
|
|
|
ImVec4 bg = iv4(t.wBgR, t.wBgG, t.wBgB, t.wBgA);
|
|
ImVec4 acc = iv4(t.accR, t.accG, t.accB, t.accA);
|
|
ImVec4 txt = iv4(t.txtR, t.txtG, t.txtB, t.txtA);
|
|
ImVec4 pop = iv4(t.popR, t.popG, t.popB, t.popA);
|
|
|
|
s.Colors[ImGuiCol_WindowBg] = bg;
|
|
s.Colors[ImGuiCol_ChildBg] = bg;
|
|
s.Colors[ImGuiCol_PopupBg] = pop;
|
|
s.Colors[ImGuiCol_Text] = txt;
|
|
s.Colors[ImGuiCol_TextDisabled] = darken(txt, 0.5f);
|
|
|
|
s.Colors[ImGuiCol_TitleBgActive] = acc;
|
|
s.Colors[ImGuiCol_TitleBg] = darken(acc, 0.55f);
|
|
s.Colors[ImGuiCol_TitleBgCollapsed] = darken(acc, 0.35f);
|
|
|
|
s.Colors[ImGuiCol_Button] = darken(acc, 0.55f);
|
|
s.Colors[ImGuiCol_ButtonHovered] = darken(acc, 0.80f);
|
|
s.Colors[ImGuiCol_ButtonActive] = acc;
|
|
|
|
s.Colors[ImGuiCol_Header] = darken(acc, 0.40f);
|
|
s.Colors[ImGuiCol_HeaderHovered] = darken(acc, 0.65f);
|
|
s.Colors[ImGuiCol_HeaderActive] = acc;
|
|
|
|
s.Colors[ImGuiCol_FrameBg] = iv4(t.wBgR+0.07f, t.wBgG+0.07f, t.wBgB+0.07f, t.wBgA);
|
|
s.Colors[ImGuiCol_FrameBgHovered] = darken(acc, 0.35f);
|
|
s.Colors[ImGuiCol_FrameBgActive] = darken(acc, 0.55f);
|
|
|
|
s.Colors[ImGuiCol_SliderGrab] = brighten(acc, 1.1f);
|
|
s.Colors[ImGuiCol_SliderGrabActive] = brighten(acc, 1.4f);
|
|
s.Colors[ImGuiCol_CheckMark] = brighten(acc, 1.2f);
|
|
|
|
s.Colors[ImGuiCol_ResizeGrip] = iv4(t.accR, t.accG, t.accB, 0.20f);
|
|
s.Colors[ImGuiCol_ResizeGripHovered] = iv4(t.accR, t.accG, t.accB, 0.67f);
|
|
s.Colors[ImGuiCol_ResizeGripActive] = acc;
|
|
|
|
s.Colors[ImGuiCol_Separator] = iv4(t.accR, t.accG, t.accB, 0.35f);
|
|
s.Colors[ImGuiCol_SeparatorHovered] = darken(acc, 0.7f);
|
|
s.Colors[ImGuiCol_SeparatorActive] = acc;
|
|
|
|
s.Colors[ImGuiCol_MenuBarBg] = darken(bg, 0.85f);
|
|
s.Colors[ImGuiCol_ScrollbarBg] = darken(bg, 0.70f);
|
|
s.Colors[ImGuiCol_ScrollbarGrab] = darken(acc, 0.55f);
|
|
s.Colors[ImGuiCol_ScrollbarGrabHovered] = darken(acc, 0.75f);
|
|
s.Colors[ImGuiCol_ScrollbarGrabActive] = acc;
|
|
}
|
|
|
|
// Snapshots g_theme under a single mutex lock, applying a pending style change first
|
|
// if g_themeChanged is set. Replaces the previous double-lock pattern (lock to apply,
|
|
// then lock again to copy). Call once at the top of each render entry point.
|
|
static MapThemeData SnapshotTheme() {
|
|
bool changed = g_themeChanged.exchange(false);
|
|
std::lock_guard<std::mutex> lk(g_themeMutex);
|
|
if (changed) ApplyThemeToStyle(g_theme);
|
|
return g_theme;
|
|
}
|
|
|
|
// Default S3 Dark theme — identical to what InitImGui used to hardcode.
|
|
static const MapThemeData kS3DarkTheme = {
|
|
0.12f, 0.12f, 0.12f, 1.00f, // windowBg
|
|
0.26f, 0.59f, 0.98f, 1.00f, // accent (ImGui default TitleBgActive blue)
|
|
1.00f, 1.00f, 1.00f, 1.00f, // text
|
|
0.08f, 0.08f, 0.08f, 0.94f, // popupBg
|
|
1.00f, 1.00f, 1.00f, 1.00f, // mapTint (no tint)
|
|
0.086f,0.086f,0.086f,0.784f, // compassBg (IM_COL32(22,22,22,200))
|
|
0.804f,0.196f,0.196f,1.00f, // needleN (IM_COL32(205,50,50,255))
|
|
0.804f,0.804f,0.804f,0.863f, // needleS (IM_COL32(205,205,205,220))
|
|
0.082f,0.122f,0.157f,1.00f, // mapBg camera clear (dark blue-gray)
|
|
};
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// ImGui initialisation (call once per D3D11 device)
|
|
// ---------------------------------------------------------------------------
|
|
static void InitImGui(ID3D11Device* device, ID3D11DeviceContext* context) {
|
|
IMGUI_CHECKVERSION();
|
|
ImGui::CreateContext();
|
|
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
io.IniFilename = nullptr; // no imgui.ini — we're a mod, not a standalone app
|
|
io.ConfigFlags |= ImGuiConfigFlags_NoMouseCursorChange; // Win32 owns the cursor
|
|
|
|
// Dark theme base — specific colours are overridden by ApplyThemeToStyle below.
|
|
ImGui::StyleColorsDark();
|
|
ImGuiStyle& style = ImGui::GetStyle();
|
|
style.WindowRounding = 0.f;
|
|
style.WindowBorderSize = 0.f;
|
|
// Allow the 22px toolbar to be exactly 22px. The default 32px minimum makes it
|
|
// overhang the bottom of the in-game map window as a stray dark strip.
|
|
style.WindowMinSize = ImVec2(4.f, 4.f);
|
|
style.FrameRounding = 3.f;
|
|
style.GrabRounding = 3.f;
|
|
style.ScrollbarRounding = 3.f;
|
|
|
|
// Apply the current theme (or the default S3 Dark if none has been pushed yet).
|
|
{ std::lock_guard<std::mutex> lk(g_themeMutex); if (g_theme.accA == 0.f) g_theme = kS3DarkTheme; }
|
|
ApplyThemeToStyle(g_theme);
|
|
|
|
// Locate ProggyClean.ttf relative to this DLL. IMGUI_DISABLE_DEFAULT_FONT strips
|
|
// the built-in base85 blob from the binary (that blob incidentally contains "UPX",
|
|
// which AV scanners mistake for a UPX-packed executable). We ship the TTF file
|
|
// alongside the DLL so end users see identical visual output.
|
|
{
|
|
char fontPath[MAX_PATH] = {};
|
|
HMODULE self = GetModuleHandleA("S3Native.dll");
|
|
if (self && GetModuleFileNameA(self, fontPath, MAX_PATH)) {
|
|
char* slash = strrchr(fontPath, '\\');
|
|
if (slash) { *++slash = '\0'; strcat_s(fontPath, "ProggyClean.ttf"); }
|
|
}
|
|
// Fallback chain: mod dir → Consolas → Lucida Console.
|
|
// At least one must succeed or ImGui renders blank glyphs (no crash).
|
|
static const char* kFallbacks[] = {
|
|
"C:\\Windows\\Fonts\\consola.ttf",
|
|
"C:\\Windows\\Fonts\\lucon.ttf",
|
|
nullptr
|
|
};
|
|
if (!fontPath[0] || !io.Fonts->AddFontFromFileTTF(fontPath, 13.f)) {
|
|
for (const char** fb = kFallbacks; *fb; ++fb)
|
|
if (io.Fonts->AddFontFromFileTTF(*fb, 13.f)) break;
|
|
}
|
|
}
|
|
{
|
|
ImFontConfig fc;
|
|
fc.MergeMode = true;
|
|
fc.GlyphMinAdvanceX = 13.f;
|
|
static const ImWchar kGlyphRanges[] = {
|
|
0x2699, 0x2699, // ⚙ gear (settings button)
|
|
0x25CE, 0x25CE, // ◎ bullseye (follow-player button)
|
|
0 };
|
|
io.Fonts->AddFontFromFileTTF("C:\\Windows\\Fonts\\seguisym.ttf",
|
|
14.f, &fc, kGlyphRanges);
|
|
// seguisym.ttf absent → AddFontFromFileTTF returns nullptr, merge is skipped
|
|
}
|
|
io.Fonts->Build();
|
|
|
|
ImGui_ImplDX11_Init(device, context);
|
|
g_imguiInited = true;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Public API
|
|
// ---------------------------------------------------------------------------
|
|
void Renderer_OnDeviceInit(ID3D11Device* device) {
|
|
if (!device) return;
|
|
g_device = device;
|
|
device->GetImmediateContext(&g_context);
|
|
|
|
// Compile blit shaders
|
|
ComPtr<ID3DBlob> vsBlob, psBlob, errBlob;
|
|
if (FAILED(D3DCompile(kBlitVS, strlen(kBlitVS), "BlitVS", nullptr, nullptr,
|
|
"main", "vs_5_0", 0, 0, &vsBlob, &errBlob))) {
|
|
if (errBlob) OutputDebugStringA((char*)errBlob->GetBufferPointer());
|
|
return;
|
|
}
|
|
if (FAILED(D3DCompile(kBlitPS, strlen(kBlitPS), "BlitPS", nullptr, nullptr,
|
|
"main", "ps_5_0", 0, 0, &psBlob, &errBlob))) {
|
|
if (errBlob) OutputDebugStringA((char*)errBlob->GetBufferPointer());
|
|
return;
|
|
}
|
|
device->CreateVertexShader(vsBlob->GetBufferPointer(), vsBlob->GetBufferSize(), nullptr, &g_vs);
|
|
device->CreatePixelShader (psBlob->GetBufferPointer(), psBlob->GetBufferSize(), nullptr, &g_ps);
|
|
|
|
D3D11_SAMPLER_DESC sd = {};
|
|
sd.Filter = D3D11_FILTER_MIN_MAG_MIP_LINEAR;
|
|
sd.AddressU = sd.AddressV = sd.AddressW = D3D11_TEXTURE_ADDRESS_CLAMP;
|
|
sd.MaxLOD = D3D11_FLOAT32_MAX;
|
|
device->CreateSamplerState(&sd, &g_sampler);
|
|
|
|
D3D11_BLEND_DESC bd = {};
|
|
bd.RenderTarget[0].BlendEnable = FALSE;
|
|
bd.RenderTarget[0].RenderTargetWriteMask = D3D11_COLOR_WRITE_ENABLE_ALL;
|
|
device->CreateBlendState(&bd, &g_blendOpaque);
|
|
|
|
D3D11_RASTERIZER_DESC rd = {};
|
|
rd.FillMode = D3D11_FILL_SOLID;
|
|
rd.CullMode = D3D11_CULL_NONE;
|
|
device->CreateRasterizerState(&rd, &g_rasterState);
|
|
|
|
D3D11_DEPTH_STENCIL_DESC dd = {};
|
|
dd.DepthEnable = FALSE;
|
|
device->CreateDepthStencilState(&dd, &g_depthState);
|
|
|
|
D3D11_BUFFER_DESC cbd = {};
|
|
cbd.ByteWidth = sizeof(UVRectCB);
|
|
cbd.Usage = D3D11_USAGE_DYNAMIC;
|
|
cbd.BindFlags = D3D11_BIND_CONSTANT_BUFFER;
|
|
cbd.CPUAccessFlags = D3D11_CPU_ACCESS_WRITE;
|
|
device->CreateBuffer(&cbd, nullptr, &g_uvRectCB);
|
|
|
|
InitImGui(device, g_context);
|
|
}
|
|
|
|
void Renderer_SetTheme(const MapThemeData* t, int presetIndex) {
|
|
if (!t) return;
|
|
{ std::lock_guard<std::mutex> lk(g_themeMutex); g_theme = *t; }
|
|
g_currentThemePreset.store(presetIndex);
|
|
g_themeChanged.store(true);
|
|
}
|
|
|
|
void Renderer_OnDeviceShutdown() {
|
|
if (g_imguiInited) {
|
|
ImGui_ImplDX11_Shutdown();
|
|
ImGui::DestroyContext();
|
|
g_imguiInited = false;
|
|
}
|
|
g_vs.Reset(); g_ps.Reset(); g_uvRectCB.Reset();
|
|
g_sampler.Reset(); g_blendOpaque.Reset();
|
|
g_rasterState.Reset(); g_depthState.Reset();
|
|
if (g_context) { g_context->Release(); g_context = nullptr; }
|
|
g_device = nullptr;
|
|
}
|
|
|
|
bool Renderer_EnsureSwapchain(PopoutWindow* win) {
|
|
if (!g_device || !win || !win->contentHwnd) return false;
|
|
int w = win->width.load(), h = win->height.load();
|
|
if (w <= 0 || h <= 0) return false;
|
|
|
|
if (win->swapChain) {
|
|
DXGI_SWAP_CHAIN_DESC desc;
|
|
win->swapChain->GetDesc(&desc);
|
|
if ((int)desc.BufferDesc.Width == w && (int)desc.BufferDesc.Height == h) return true;
|
|
g_context->OMSetRenderTargets(0, nullptr, nullptr);
|
|
g_context->Flush();
|
|
if (win->rtv) { win->rtv->Release(); win->rtv = nullptr; }
|
|
if (win->swapChain) { win->swapChain->Release(); win->swapChain = nullptr; }
|
|
}
|
|
|
|
ComPtr<IDXGIDevice> dxgiDevice; ComPtr<IDXGIAdapter> adapter; ComPtr<IDXGIFactory> factory;
|
|
if (FAILED(g_device->QueryInterface(__uuidof(IDXGIDevice), (void**)&dxgiDevice))) return false;
|
|
if (FAILED(dxgiDevice->GetAdapter(&adapter))) return false;
|
|
if (FAILED(adapter->GetParent(__uuidof(IDXGIFactory), (void**)&factory))) return false;
|
|
|
|
DXGI_SWAP_CHAIN_DESC scd = {};
|
|
scd.BufferCount = 2;
|
|
scd.BufferDesc.Width = static_cast<UINT>(w);
|
|
scd.BufferDesc.Height = static_cast<UINT>(h);
|
|
scd.BufferDesc.Format = DXGI_FORMAT_R8G8B8A8_UNORM;
|
|
scd.BufferUsage = DXGI_USAGE_RENDER_TARGET_OUTPUT;
|
|
scd.OutputWindow = win->contentHwnd;
|
|
scd.SampleDesc.Count = 1;
|
|
scd.Windowed = TRUE;
|
|
scd.SwapEffect = DXGI_SWAP_EFFECT_DISCARD;
|
|
scd.Flags = DXGI_SWAP_CHAIN_FLAG_ALLOW_MODE_SWITCH;
|
|
if (FAILED(factory->CreateSwapChain(g_device, &scd, &win->swapChain))) return false;
|
|
factory->MakeWindowAssociation(win->contentHwnd, DXGI_MWA_NO_ALT_ENTER);
|
|
|
|
ID3D11Texture2D* bb = nullptr;
|
|
win->swapChain->GetBuffer(0, __uuidof(ID3D11Texture2D), (void**)&bb);
|
|
if (bb) { g_device->CreateRenderTargetView(bb, nullptr, &win->rtv); bb->Release(); }
|
|
return win->rtv != nullptr;
|
|
}
|
|
|
|
void Renderer_ReleaseSwapchain(PopoutWindow* win) {
|
|
if (!win) return;
|
|
if (win->rtv) { win->rtv->Release(); win->rtv = nullptr; }
|
|
if (win->swapChain) { win->swapChain->Release(); win->swapChain = nullptr; }
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Shared map UI chrome (compass rose + bottom toolbar), drawn for window `win`
|
|
// at logical size w x h. Pure ImGui: reads win-> state atomics and pushes
|
|
// UICommands onto win->inputQueue. The caller owns the frame lifecycle — it must
|
|
// set up io, call ImGui_ImplDX11_NewFrame()/ImGui::NewFrame() before this, and
|
|
// ImGui::Render() after. Used by the popout (Renderer_Present) and, in time, by
|
|
// the in-game overlay so the map UI is authored once and rendered on any surface.
|
|
// ---------------------------------------------------------------------------
|
|
// origin = top-left screen position of the map region; w,h = its size. For the
|
|
// popout the region is the whole window (origin 0,0); for the in-game overlay it
|
|
// is the floating window's map image rect, so the chrome tracks that window.
|
|
// reserveResizeGrip: shorten the toolbar so the in-game ImGui window's bottom-right
|
|
// resize grip stays grabbable (the popout uses its OS window border instead).
|
|
static void BuildMapUI(PopoutWindow* win, ImVec2 origin, float w, float h,
|
|
bool reserveResizeGrip, bool showPopOutBtn,
|
|
const MapThemeData& theme, float mapAlpha = 1.0f) {
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
|
|
// Shared command helper — usable from any window in this frame
|
|
auto pushCmd = [&](UICmd cmd, float idx = 0.f) {
|
|
InputEvent ev{}; ev.type = UICommand;
|
|
ev.x = static_cast<float>(cmd); ev.y = idx;
|
|
win->inputQueue.push(ev);
|
|
};
|
|
|
|
const float kBarH = 22.f;
|
|
|
|
// ── Compass overlay (floating in the bottom-right of the map area) ──
|
|
const float kCR = 36.f; // compass circle radius
|
|
const float kCWin = kCR * 2.f + 20.f; // window size (leaves room for N label)
|
|
const float kCPad = 10.f; // margin from window edge
|
|
|
|
if (h > kBarH + kCWin + kCPad * 2.f) { // skip if window too small
|
|
ImGui::SetNextWindowPos(
|
|
ImVec2(origin.x + w - kCWin - kCPad, origin.y + h - kBarH - kCWin - kCPad),
|
|
ImGuiCond_Always);
|
|
ImGui::SetNextWindowSize(ImVec2(kCWin, kCWin));
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.f, 0.f));
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.f);
|
|
ImGui::Begin("##compass_overlay", nullptr,
|
|
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
|
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
|
|
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoNav |
|
|
ImGuiWindowFlags_NoFocusOnAppearing | ImGuiWindowFlags_NoBackground);
|
|
|
|
ImGui::SetCursorPos(ImVec2(0.f, 0.f));
|
|
ImGui::InvisibleButton("##compassHit", ImVec2(kCWin, kCWin));
|
|
bool cHov = ImGui::IsItemHovered();
|
|
bool cAct = ImGui::IsItemActive();
|
|
|
|
// Drag to rotate: angle from compass centre → new bearing
|
|
if (cAct) {
|
|
ImVec2 wp = ImGui::GetWindowPos();
|
|
ImVec2 cc = { wp.x + kCWin * 0.5f, wp.y + kCWin * 0.5f };
|
|
float ddx = io.MousePos.x - cc.x, ddy = io.MousePos.y - cc.y;
|
|
if (ddx*ddx + ddy*ddy > 9.f) {
|
|
float ang = atan2f(ddx, -ddy) * (180.f / 3.14159265f);
|
|
if (ang < 0.f) ang += 360.f;
|
|
win->imMapRotationDeg.store(ang);
|
|
win->imMapSyncPlayer.store(false);
|
|
pushCmd(UICmd::SetRotation, ang);
|
|
}
|
|
}
|
|
if (ImGui::IsItemClicked(ImGuiMouseButton_Right)) {
|
|
win->imMapRotationDeg.store(0.f);
|
|
win->imMapSyncPlayer.store(false);
|
|
pushCmd(UICmd::RotateReset);
|
|
}
|
|
|
|
// Draw compass rose
|
|
{
|
|
float rotDeg = win->imMapRotationDeg.load();
|
|
float rotRad = rotDeg * (3.14159265f / 180.f);
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
ImVec2 wp = ImGui::GetWindowPos();
|
|
ImVec2 cc = { wp.x + kCWin * 0.5f, wp.y + kCWin * 0.5f };
|
|
|
|
// Derive compass colours from theme; scale alpha by mapAlpha so the
|
|
// compass responds to the Map Elements opacity slider.
|
|
auto col32 = [&](float r, float g, float b, float a) -> ImU32 {
|
|
return IM_COL32((int)(r*255), (int)(g*255), (int)(b*255), (int)(a * mapAlpha * 255));
|
|
};
|
|
ImU32 bgCol = cAct ? col32(theme.cmpR*1.6f, theme.cmpG*1.6f, theme.cmpB*1.6f, theme.cmpA)
|
|
: cHov ? col32(theme.cmpR*1.3f, theme.cmpG*1.3f, theme.cmpB*1.3f, theme.cmpA)
|
|
: col32(theme.cmpR, theme.cmpG, theme.cmpB, theme.cmpA);
|
|
ImU32 ringCol = col32(theme.accR*0.5f, theme.accG*0.5f, theme.accB*0.5f, 0.78f);
|
|
ImU32 tickCard = col32(theme.txtR*0.67f, theme.txtG*0.67f, theme.txtB*0.67f, 0.86f);
|
|
ImU32 tickIter = col32(theme.txtR*0.50f, theme.txtG*0.50f, theme.txtB*0.50f, 0.51f);
|
|
ImU32 needleN = col32(theme.nNR, theme.nNG, theme.nNB, theme.nNA);
|
|
ImU32 needleS = col32(theme.nSR, theme.nSG, theme.nSB, theme.nSA);
|
|
ImU32 labelN = col32(std::min(1.f,theme.nNR*1.1f), std::min(1.f,theme.nNG*1.1f),
|
|
std::min(1.f,theme.nNB*1.1f), 1.0f);
|
|
ImU32 capOuter = col32(theme.cmpR*0.85f, theme.cmpG*0.85f, theme.cmpB*0.85f, 1.0f);
|
|
ImU32 capInner = col32(theme.txtR*0.82f, theme.txtG*0.82f, theme.txtB*0.82f, 1.0f);
|
|
|
|
// Background disk
|
|
dl->AddCircleFilled(cc, kCR + 2.f, bgCol, 48);
|
|
dl->AddCircle(cc, kCR + 2.f, ringCol, 48, 1.5f);
|
|
|
|
// Tick marks: 8 directions — cardinals longer, inter-cardinals shorter
|
|
for (int i = 0; i < 8; ++i) {
|
|
float a = (i * 45.f - rotDeg) * (3.14159265f / 180.f);
|
|
float ox = sinf(a), oy = -cosf(a);
|
|
bool card = (i % 2 == 0);
|
|
float inner = kCR - (card ? 9.f : 5.f);
|
|
dl->AddLine({ cc.x + ox*inner, cc.y + oy*inner },
|
|
{ cc.x + ox*(kCR - 1.f), cc.y + oy*(kCR - 1.f) },
|
|
card ? tickCard : tickIter,
|
|
card ? 1.5f : 1.f);
|
|
}
|
|
|
|
// Needle: diamond split N / S colours from theme
|
|
float nx = sinf(rotRad), ny = -cosf(rotRad);
|
|
float bx = -ny * 7.f, by = nx * 7.f;
|
|
ImVec2 nTip = { cc.x + nx*(kCR-5.f), cc.y + ny*(kCR-5.f) };
|
|
ImVec2 sTail = { cc.x - nx*(kCR-5.f)*0.55f, cc.y - ny*(kCR-5.f)*0.55f };
|
|
ImVec2 bleft = { cc.x + bx, cc.y + by };
|
|
ImVec2 brght = { cc.x - bx, cc.y - by };
|
|
|
|
dl->AddTriangleFilled(nTip, bleft, brght, needleN);
|
|
dl->AddTriangleFilled(sTail, bleft, brght, needleS);
|
|
dl->AddTriangle(nTip, bleft, brght, IM_COL32(0,0,0,100), 0.5f);
|
|
dl->AddTriangle(sTail, bleft, brght, IM_COL32(0,0,0, 80), 0.5f);
|
|
|
|
// Centre cap
|
|
dl->AddCircleFilled(cc, 5.5f, capOuter);
|
|
dl->AddCircleFilled(cc, 3.5f, capInner);
|
|
|
|
// "N" label at needle tip
|
|
ImVec2 nPos = { cc.x + nx*(kCR + 4.f) - 4.f, cc.y + ny*(kCR + 4.f) - 7.f };
|
|
dl->AddText(ImGui::GetFont(), 14.f, nPos, labelN, "N");
|
|
}
|
|
|
|
if (cHov)
|
|
ImGui::SetTooltip("Rotation: %.0f\xc2\xb0\nDrag to rotate | Right-click to reset",
|
|
win->imMapRotationDeg.load());
|
|
|
|
ImGui::End();
|
|
ImGui::PopStyleVar(2);
|
|
}
|
|
|
|
// ── Toolbar strip ────────────────────────────────────────────────────
|
|
// Leave the bottom-right corner clear for the ImGui resize grip when in-game.
|
|
const float kGripReserve = reserveResizeGrip ? 18.f : 0.f;
|
|
ImGui::SetNextWindowPos(ImVec2(origin.x, origin.y + (float)h - kBarH));
|
|
ImGui::SetNextWindowSize(ImVec2((float)w - kGripReserve, kBarH));
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(6.f, 2.f));
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.f);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(4.f, 2.f));
|
|
ImGui::Begin("##toolbar", nullptr,
|
|
ImGuiWindowFlags_NoTitleBar | ImGuiWindowFlags_NoResize |
|
|
ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoScrollbar |
|
|
ImGuiWindowFlags_NoSavedSettings | ImGuiWindowFlags_NoNav |
|
|
ImGuiWindowFlags_NoFocusOnAppearing);
|
|
|
|
// Status label (left)
|
|
{
|
|
char buf[256];
|
|
{ std::lock_guard<std::mutex> lk(win->imStatusMutex); strncpy_s(buf, win->imStatusText, _TRUNCATE); }
|
|
ImGui::TextUnformatted(buf);
|
|
}
|
|
|
|
const float kBtnH = kBarH - 4.f;
|
|
const float kBtnW = 22.f;
|
|
const float kGap = 4.f;
|
|
|
|
// Toolbar buttons use the accent colour from the current theme. Active (lit)
|
|
// state is a 40% brighter version of the accent.
|
|
const ImVec4 kBtnBlue = ImGui::GetStyleColorVec4(ImGuiCol_TitleBgActive);
|
|
const ImVec4 kBtnBlueActive = ImVec4(
|
|
std::min(1.f, kBtnBlue.x * 1.4f),
|
|
std::min(1.f, kBtnBlue.y * 1.4f),
|
|
std::min(1.f, kBtnBlue.z * 1.4f), 1.f);
|
|
|
|
// Luminance-based contrast text: same formula as Railroader's ColorHelper.IsDark()
|
|
auto contrastText = [](ImVec4 bg) -> ImVec4 {
|
|
float L = 0.299f*bg.x + 0.587f*bg.y + 0.114f*bg.z;
|
|
return L > 0.45f ? ImVec4(0.10f, 0.10f, 0.10f, 1.f)
|
|
: ImVec4(0.95f, 0.95f, 0.95f, 1.f);
|
|
};
|
|
|
|
// >> Pop Out (in-game overlay only) — switches map to the OS popout window
|
|
if (showPopOutBtn) {
|
|
ImGui::SameLine(ImGui::GetContentRegionMax().x - kBtnW - kGap - kBtnW - kGap - kBtnW);
|
|
ImGui::PushStyleColor(ImGuiCol_Button, kBtnBlue);
|
|
ImGui::PushStyleColor(ImGuiCol_Text, contrastText(kBtnBlue));
|
|
if (ImGui::Button(">>##popout", ImVec2(kBtnW, kBtnH)))
|
|
pushCmd(UICmd::PopOut);
|
|
ImGui::PopStyleColor(2);
|
|
if (ImGui::IsItemHovered())
|
|
ImGui::SetTooltip("Open in popout window");
|
|
}
|
|
|
|
// ◎ Follow-player position toggle
|
|
ImGui::SameLine(ImGui::GetContentRegionMax().x - kBtnW - kGap - kBtnW);
|
|
{
|
|
bool fOn = win->imFollowPlayer.load();
|
|
ImVec4 fColor = fOn ? kBtnBlueActive : kBtnBlue;
|
|
ImGui::PushStyleColor(ImGuiCol_Button, fColor);
|
|
ImGui::PushStyleColor(ImGuiCol_Text, contrastText(fColor));
|
|
if (ImGui::Button("\xe2\x97\x8e##follow", ImVec2(kBtnW, kBtnH)))
|
|
pushCmd(UICmd::ToggleFollowPlayer);
|
|
ImGui::PopStyleColor(2);
|
|
if (ImGui::IsItemHovered())
|
|
ImGui::SetTooltip(fOn ? "Following player position\nClick to stop"
|
|
: "Follow player position");
|
|
}
|
|
|
|
// ⚙ Settings gear
|
|
ImGui::SameLine(ImGui::GetContentRegionMax().x - kBtnW);
|
|
ImGui::PushStyleColor(ImGuiCol_Button, kBtnBlue);
|
|
ImGui::PushStyleColor(ImGuiCol_Text, contrastText(kBtnBlue));
|
|
if (ImGui::Button("\xe2\x9a\x99##cfg", ImVec2(kBtnW, kBtnH)))
|
|
ImGui::OpenPopup("##settings");
|
|
ImGui::PopStyleColor(2);
|
|
|
|
if (ImGui::BeginPopup("##settings")) {
|
|
if (ImGui::MenuItem("Center on player"))
|
|
pushCmd(UICmd::Recenter);
|
|
|
|
// Sync rotation — checkmark shows live state
|
|
if (ImGui::MenuItem("Sync rotation to player camera", nullptr,
|
|
win->imMapSyncPlayer.load()))
|
|
pushCmd(UICmd::RotateSyncPlayer);
|
|
|
|
if (ImGui::MenuItem("Pan cancels follow", nullptr,
|
|
win->imPanDisablesFollow.load()))
|
|
pushCmd(UICmd::TogglePanDisablesFollow);
|
|
|
|
if (ImGui::MenuItem("Right-click recenters", nullptr,
|
|
win->imRightClickRecenter.load()))
|
|
pushCmd(UICmd::ToggleRightClickRecenter);
|
|
|
|
ImGui::Separator();
|
|
|
|
if (ImGui::BeginMenu("Icons")) {
|
|
bool cullOn = win->imIconCullingEnabled.load();
|
|
if (ImGui::MenuItem("Hide cars when zoomed out", nullptr, cullOn)) {
|
|
win->imIconCullingEnabled.store(!cullOn);
|
|
pushCmd(UICmd::ToggleIconCulling);
|
|
}
|
|
|
|
ImGui::BeginDisabled(!cullOn);
|
|
|
|
// Display as percentage (0.40 → 40%); store as fraction.
|
|
float threshPct = win->imCullThreshold.load() * 100.f;
|
|
ImGui::SetNextItemWidth(100.f);
|
|
if (ImGui::SliderFloat("Cull threshold##icons", &threshPct, 30.f, 100.f, "%.0f%%"))
|
|
win->imCullThreshold.store(threshPct / 100.f);
|
|
if (ImGui::IsItemDeactivatedAfterEdit())
|
|
pushCmd(UICmd::SetCullThreshold, win->imCullThreshold.load());
|
|
|
|
ImGui::Separator();
|
|
|
|
bool hideMU = win->imHideMuLocos.load();
|
|
if (ImGui::MenuItem("Hide MU trailing units", nullptr, hideMU)) {
|
|
win->imHideMuLocos.store(!hideMU);
|
|
pushCmd(UICmd::ToggleHideMuLocos);
|
|
}
|
|
|
|
float boost = win->imLocoBoostedScale.load();
|
|
ImGui::SetNextItemWidth(100.f);
|
|
if (ImGui::SliderFloat("Loco icon boost##icons", &boost, 1.0f, 3.0f, "%.1fx"))
|
|
win->imLocoBoostedScale.store(boost);
|
|
if (ImGui::IsItemDeactivatedAfterEdit())
|
|
pushCmd(UICmd::SetLocoBoostedScale, win->imLocoBoostedScale.load());
|
|
|
|
ImGui::Separator();
|
|
|
|
bool eotdOn = win->imEotdEnabled.load();
|
|
if (ImGui::MenuItem("Show EOTD", nullptr, eotdOn)) {
|
|
win->imEotdEnabled.store(!eotdOn);
|
|
pushCmd(UICmd::ToggleEotd);
|
|
}
|
|
|
|
ImGui::BeginDisabled(!eotdOn);
|
|
|
|
bool eotdWC = win->imEotdOnlyWhenCulled.load();
|
|
if (ImGui::MenuItem("Hide EOTD when icons visible", nullptr, eotdWC)) {
|
|
win->imEotdOnlyWhenCulled.store(!eotdWC);
|
|
pushCmd(UICmd::ToggleEotdOnlyWhenCulled);
|
|
}
|
|
|
|
float eotdSz = win->imEotdSizeScale.load();
|
|
ImGui::SetNextItemWidth(100.f);
|
|
if (ImGui::SliderFloat("EOTD dot size##icons", &eotdSz, 1.0f, 10.0f, "%.1f"))
|
|
win->imEotdSizeScale.store(eotdSz);
|
|
if (ImGui::IsItemDeactivatedAfterEdit())
|
|
pushCmd(UICmd::SetEotdSizeScale, win->imEotdSizeScale.load());
|
|
|
|
ImGui::EndDisabled(); // !eotdOn
|
|
ImGui::EndDisabled(); // !cullOn
|
|
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
ImGui::Separator();
|
|
|
|
// Helper lambdas for ME setting commands (y = index, delta = value)
|
|
auto pushMEBool = [&](int idx, bool val) {
|
|
InputEvent ev{}; ev.type = UICommand;
|
|
ev.x = static_cast<float>(UICmd::SetMEBool);
|
|
ev.y = static_cast<float>(idx);
|
|
ev.delta = val ? 1.f : 0.f;
|
|
win->inputQueue.push(ev);
|
|
};
|
|
auto pushMEFloat = [&](int idx, float val) {
|
|
InputEvent ev{}; ev.type = UICommand;
|
|
ev.x = static_cast<float>(UICmd::SetMEFloat);
|
|
ev.y = static_cast<float>(idx);
|
|
ev.delta = val;
|
|
win->inputQueue.push(ev);
|
|
};
|
|
|
|
bool meOn = win->imMapEnhancerInstalled.load();
|
|
bool meTurntable = win->imMEHasTurntable.load(); // v1.6.0 or community
|
|
bool meCom = win->imMECommunity.load(); // community only
|
|
if (meOn) {
|
|
if (ImGui::BeginMenu("Map Enhancer")) {
|
|
if (ImGui::MenuItem("Toggle follow mode"))
|
|
pushCmd(UICmd::FollowMode);
|
|
ImGui::Separator();
|
|
if (ImGui::BeginMenu("Follow")) {
|
|
if (ImGui::MenuItem("Player camera"))
|
|
pushCmd(UICmd::FollowPlayerCam);
|
|
if (ImGui::MenuItem("Selected loco"))
|
|
pushCmd(UICmd::FollowSelectedLoco);
|
|
if (ImGui::MenuItem("Stop following"))
|
|
pushCmd(UICmd::FollowMode);
|
|
{
|
|
std::lock_guard<std::mutex> lk(win->imLocoMutex);
|
|
if (!win->imLocoList.empty()) {
|
|
ImGui::Separator();
|
|
for (int i = 0; i < (int)win->imLocoList.size(); ++i)
|
|
if (ImGui::MenuItem(win->imLocoList[i].label))
|
|
pushCmd(UICmd::FollowLoco, (float)i);
|
|
}
|
|
}
|
|
ImGui::EndMenu();
|
|
}
|
|
if (ImGui::BeginMenu("Jump to Location")) {
|
|
std::lock_guard<std::mutex> lk(win->imLocationMutex);
|
|
if (win->imLocationList.empty())
|
|
ImGui::TextDisabled("(loading...)");
|
|
else
|
|
for (int i = 0; i < (int)win->imLocationList.size(); ++i)
|
|
if (ImGui::MenuItem(win->imLocationList[i].label))
|
|
pushCmd(UICmd::JumpToLocation, (float)i);
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
ImGui::Separator();
|
|
|
|
if (ImGui::BeginMenu("Settings")) {
|
|
uint32_t meFlags = win->imMEFlags.load();
|
|
auto meBit = [&](int b) { return (meFlags & (1u << b)) != 0; };
|
|
|
|
// -- Markers --
|
|
// Turntable Markers: available on v1.6.0 and community (ShowTurntableMarkers field).
|
|
// Everything else in this block is community-only.
|
|
ImGui::TextDisabled("Markers");
|
|
ImGui::BeginDisabled(!meTurntable);
|
|
{
|
|
bool v = meBit(MB_TurntableMarkers);
|
|
if (ImGui::MenuItem("Turntable Markers", nullptr, v)) pushMEBool(MB_TurntableMarkers, !v);
|
|
}
|
|
ImGui::EndDisabled();
|
|
ImGui::BeginDisabled(!meCom);
|
|
{
|
|
bool v = meBit(MB_TurntableControl);
|
|
if (ImGui::MenuItem("Turntable Control", nullptr, v)) pushMEBool(MB_TurntableControl, !v);
|
|
if (v) {
|
|
bool cl = meBit(MB_CheckClearance);
|
|
if (ImGui::MenuItem(" Check Clearance", nullptr, cl)) pushMEBool(MB_CheckClearance, !cl);
|
|
}
|
|
}
|
|
{
|
|
bool v = meBit(MB_CrossingMarkers);
|
|
if (ImGui::MenuItem("Road Crossing Markers", nullptr, v)) pushMEBool(MB_CrossingMarkers, !v);
|
|
}
|
|
{
|
|
bool v = meBit(MB_PassengerStops);
|
|
if (ImGui::MenuItem("Passenger Stop Tracking", nullptr, v)) pushMEBool(MB_PassengerStops, !v);
|
|
}
|
|
{
|
|
bool v = meBit(MB_IndustryAreaColors);
|
|
if (ImGui::MenuItem("Industry Area Colors", nullptr, v)) pushMEBool(MB_IndustryAreaColors, !v);
|
|
}
|
|
ImGui::EndDisabled();
|
|
|
|
ImGui::Separator();
|
|
|
|
// -- Behavior --
|
|
ImGui::TextDisabled("Behavior");
|
|
ImGui::BeginDisabled(!meCom);
|
|
{
|
|
bool v = meBit(MB_ModdedSpawnPoints);
|
|
if (ImGui::MenuItem("Modded Spawn Points", nullptr, v)) pushMEBool(MB_ModdedSpawnPoints, !v);
|
|
}
|
|
ImGui::EndDisabled();
|
|
{
|
|
bool v = meBit(MB_DoubleClick);
|
|
if (ImGui::MenuItem("Require Double Click", nullptr, v)) pushMEBool(MB_DoubleClick, !v);
|
|
}
|
|
|
|
ImGui::Separator();
|
|
|
|
// -- Scales -- (live-update atomic during drag; push on release)
|
|
ImGui::TextDisabled("Scales");
|
|
{
|
|
float v = win->imMEFlareScale.load();
|
|
ImGui::SetNextItemWidth(110.f);
|
|
if (ImGui::SliderFloat("Flare##me", &v, 0.1f, 1.0f, "%.2f"))
|
|
win->imMEFlareScale.store(v);
|
|
if (ImGui::IsItemDeactivatedAfterEdit())
|
|
pushMEFloat(MF_FlareScale, win->imMEFlareScale.load());
|
|
}
|
|
{
|
|
float v = win->imMEJunctionSc.load();
|
|
ImGui::SetNextItemWidth(110.f);
|
|
if (ImGui::SliderFloat("Junction##me", &v, 0.5f, 1.0f, "%.2f"))
|
|
win->imMEJunctionSc.store(v);
|
|
if (ImGui::IsItemDeactivatedAfterEdit())
|
|
pushMEFloat(MF_JunctionScale, win->imMEJunctionSc.load());
|
|
}
|
|
{
|
|
float v = win->imMETrackThick.load();
|
|
ImGui::SetNextItemWidth(110.f);
|
|
if (ImGui::SliderFloat("Track Width##me", &v, 0.5f, 2.0f, "%.2f"))
|
|
win->imMETrackThick.store(v);
|
|
if (ImGui::IsItemDeactivatedAfterEdit())
|
|
pushMEFloat(MF_TrackThickness, win->imMETrackThick.load());
|
|
}
|
|
ImGui::BeginDisabled(!meCom);
|
|
{
|
|
float v = win->imMECrossingSc.load();
|
|
ImGui::SetNextItemWidth(110.f);
|
|
if (ImGui::SliderFloat("Crossing##me", &v, 0.1f, 1.0f, "%.2f"))
|
|
win->imMECrossingSc.store(v);
|
|
if (ImGui::IsItemDeactivatedAfterEdit())
|
|
pushMEFloat(MF_CrossingScale, win->imMECrossingSc.load());
|
|
}
|
|
ImGui::EndDisabled();
|
|
|
|
ImGui::Separator();
|
|
|
|
// -- Switch Reset (community build only) --
|
|
ImGui::BeginDisabled(!meCom);
|
|
ImGui::TextDisabled("Switch Reset");
|
|
if (ImGui::MenuItem("All Switches - Normal"))
|
|
pushCmd(UICmd::MEResetSwitchesNormal);
|
|
if (ImGui::MenuItem("All Switches - Thrown"))
|
|
pushCmd(UICmd::MEResetSwitchesThrown);
|
|
ImGui::EndDisabled();
|
|
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
ImGui::EndMenu();
|
|
}
|
|
} else {
|
|
if (ImGui::BeginMenu("Jump to Location")) {
|
|
std::lock_guard<std::mutex> lk(win->imLocationMutex);
|
|
if (win->imLocationList.empty())
|
|
ImGui::TextDisabled("(loading...)");
|
|
else
|
|
for (int i = 0; i < (int)win->imLocationList.size(); ++i)
|
|
if (ImGui::MenuItem(win->imLocationList[i].label))
|
|
pushCmd(UICmd::JumpToLocation, (float)i);
|
|
ImGui::EndMenu();
|
|
}
|
|
}
|
|
|
|
ImGui::Separator();
|
|
|
|
// Theme preset picker
|
|
if (ImGui::BeginMenu("Theme")) {
|
|
int cur = g_currentThemePreset.load();
|
|
if (ImGui::MenuItem("S3 Dark", nullptr, cur == 0)) pushCmd(UICmd::SetTheme, 0.f);
|
|
if (ImGui::MenuItem("Railroader Classic", nullptr, cur == 1)) pushCmd(UICmd::SetTheme, 1.f);
|
|
if (ImGui::MenuItem("Night Mode", nullptr, cur == 2)) pushCmd(UICmd::SetTheme, 2.f);
|
|
if (ImGui::MenuItem("High Visibility", nullptr, cur == 3)) pushCmd(UICmd::SetTheme, 3.f);
|
|
if (ImGui::MenuItem("Retro Terminal", nullptr, cur == 4)) pushCmd(UICmd::SetTheme, 4.f);
|
|
ImGui::Separator();
|
|
if (ImGui::MenuItem("Custom", nullptr, cur == 5)) pushCmd(UICmd::SetTheme, 5.f);
|
|
ImGui::EndMenu();
|
|
}
|
|
|
|
// Opacity sliders (in-game overlay only — popout is a native OS window)
|
|
if (showPopOutBtn) {
|
|
ImGui::Separator();
|
|
// Chrome alpha — window frame, toolbar, compass
|
|
float alpha = g_ovAlpha.load();
|
|
ImGui::SetNextItemWidth(100.f);
|
|
if (ImGui::SliderFloat("Window##ov", &alpha, 0.1f, 1.0f, "%.2f"))
|
|
g_ovAlpha.store(alpha);
|
|
if (ImGui::IsItemDeactivatedAfterEdit())
|
|
pushCmd(UICmd::SetAlpha, g_ovAlpha.load());
|
|
// Map image alpha — independent of chrome
|
|
float mAlpha = g_mapAlpha.load();
|
|
ImGui::SetNextItemWidth(100.f);
|
|
if (ImGui::SliderFloat("Map Elements##ov", &mAlpha, 0.0f, 1.0f, "%.2f"))
|
|
g_mapAlpha.store(mAlpha);
|
|
if (ImGui::IsItemDeactivatedAfterEdit())
|
|
pushCmd(UICmd::SetMapAlpha, g_mapAlpha.load());
|
|
// Map background alpha — camera clear colour opacity (0 = transparent void)
|
|
float bgAlpha = g_mapBgAlpha.load();
|
|
ImGui::SetNextItemWidth(100.f);
|
|
if (ImGui::SliderFloat("Map Background##ov", &bgAlpha, 0.0f, 1.0f, "%.2f"))
|
|
g_mapBgAlpha.store(bgAlpha);
|
|
if (ImGui::IsItemDeactivatedAfterEdit())
|
|
pushCmd(UICmd::SetMapBgAlpha, g_mapBgAlpha.load());
|
|
}
|
|
|
|
ImGui::EndPopup();
|
|
}
|
|
|
|
ImGui::PopStyleVar(3);
|
|
ImGui::End();
|
|
}
|
|
|
|
void Renderer_Present(PopoutWindow* win) {
|
|
if (!win || win->resizing.load()) return;
|
|
void* rawTex = win->pendingTexture.load();
|
|
if (!rawTex) return;
|
|
|
|
// Lazy device init from the first texture if UnityPluginLoad didn't fire
|
|
if (!g_device) {
|
|
auto* tex = static_cast<ID3D11Texture2D*>(rawTex);
|
|
ID3D11Device* dev = nullptr;
|
|
tex->GetDevice(&dev);
|
|
if (!dev) return;
|
|
Renderer_OnDeviceInit(dev);
|
|
dev->Release();
|
|
}
|
|
if (!g_device || !g_context) return;
|
|
if (!Renderer_EnsureSwapchain(win)) return;
|
|
|
|
auto* srcTex = static_cast<ID3D11Texture2D*>(rawTex);
|
|
D3D11_TEXTURE2D_DESC texDesc;
|
|
srcTex->GetDesc(&texDesc);
|
|
|
|
D3D11_SHADER_RESOURCE_VIEW_DESC srvDesc = {};
|
|
srvDesc.Format = TypedFormat(texDesc.Format);
|
|
srvDesc.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
|
|
srvDesc.Texture2D.MostDetailedMip = 0;
|
|
srvDesc.Texture2D.MipLevels = 1;
|
|
ComPtr<ID3D11ShaderResourceView> srv;
|
|
if (FAILED(g_device->CreateShaderResourceView(srcTex, &srvDesc, &srv))) return;
|
|
|
|
// Save Unity's render state
|
|
D3D11StateBlock saved;
|
|
saved.Capture(g_context);
|
|
|
|
int w = win->width.load(), h = win->height.load();
|
|
D3D11_VIEWPORT vp { 0.f, 0.f, (float)w, (float)h, 0.f, 1.f };
|
|
|
|
// Update UV rect constant buffer
|
|
if (g_uvRectCB) {
|
|
D3D11_MAPPED_SUBRESOURCE mapped = {};
|
|
if (SUCCEEDED(g_context->Map(g_uvRectCB.Get(), 0, D3D11_MAP_WRITE_DISCARD, 0, &mapped))) {
|
|
auto* cb = static_cast<UVRectCB*>(mapped.pData);
|
|
cb->u0 = win->srcU0.load(); cb->v0 = win->srcV0.load();
|
|
cb->u1 = win->srcU1.load(); cb->v1 = win->srcV1.load();
|
|
g_context->Unmap(g_uvRectCB.Get(), 0);
|
|
}
|
|
}
|
|
|
|
// Blit map texture into swapchain back buffer
|
|
float bf[4] = {};
|
|
g_context->OMSetRenderTargets(1, &win->rtv, nullptr);
|
|
g_context->RSSetViewports(1, &vp);
|
|
g_context->VSSetShader(g_vs.Get(), nullptr, 0);
|
|
ID3D11Buffer* cbPtr = g_uvRectCB.Get();
|
|
g_context->VSSetConstantBuffers(0, 1, &cbPtr);
|
|
g_context->PSSetShader(g_ps.Get(), nullptr, 0);
|
|
g_context->IASetInputLayout(nullptr);
|
|
g_context->IASetPrimitiveTopology(D3D11_PRIMITIVE_TOPOLOGY_TRIANGLELIST);
|
|
ID3D11ShaderResourceView* srvPtr = srv.Get();
|
|
g_context->PSSetShaderResources(0, 1, &srvPtr);
|
|
ID3D11SamplerState* sampPtr = g_sampler.Get();
|
|
g_context->PSSetSamplers(0, 1, &sampPtr);
|
|
g_context->OMSetBlendState(g_blendOpaque.Get(), bf, 0xFFFFFFFF);
|
|
g_context->RSSetState(g_rasterState.Get());
|
|
g_context->OMSetDepthStencilState(g_depthState.Get(), 0);
|
|
g_context->Draw(3, 0);
|
|
|
|
// -----------------------------------------------------------------------
|
|
// ImGui overlay — compass rose on map + toolbar strip at bottom
|
|
// -----------------------------------------------------------------------
|
|
if (g_imguiInited) {
|
|
MapThemeData theme = SnapshotTheme();
|
|
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
io.DisplaySize = ImVec2((float)w, (float)h);
|
|
io.MousePos = ImVec2(win->imMouseX.load(), win->imMouseY.load());
|
|
io.MouseDown[0] = win->imLButton.load();
|
|
io.MouseDown[1] = win->imRButton.load();
|
|
int rawWheel = win->imWheelRaw.exchange(0);
|
|
io.MouseWheel = (float)rawWheel / WHEEL_DELTA;
|
|
|
|
ImGui_ImplDX11_NewFrame();
|
|
ImGui::NewFrame();
|
|
|
|
BuildMapUI(win, ImVec2(0.f, 0.f), (float)w, (float)h,
|
|
/*reserveResizeGrip=*/false, /*showPopOutBtn=*/false, theme);
|
|
|
|
ImGui::Render();
|
|
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
|
|
win->imWantMouse.store(ImGui::GetIO().WantCaptureMouse);
|
|
}
|
|
|
|
win->swapChain->Present(0, 0);
|
|
|
|
// Restore Unity's render state
|
|
saved.Restore(g_context);
|
|
}
|
|
|
|
// ===========================================================================
|
|
// In-game overlay (spike)
|
|
// ===========================================================================
|
|
// Renders an ImGui test window into Unity's currently bound backbuffer (no
|
|
// swapchain, no Present) to prove the on-top render path. Reuses the SAME ImGui
|
|
// context + DX11 backend the popout uses (set up once in InitImGui): the popout
|
|
// and overlay each do a complete NewFrame/Render inside their own plugin event
|
|
// on the render thread, so sequential reuse of one context is safe. A second
|
|
// context tripped the DX11 backend's per-context init in release builds.
|
|
// ---------------------------------------------------------------------------
|
|
static std::atomic<void*> g_ovDeviceTex {nullptr};
|
|
static std::atomic<float> g_ovW {0.f}, g_ovH {0.f};
|
|
static std::atomic<float> g_ovMouseX {-1.f}, g_ovMouseY {-1.f};
|
|
static std::atomic<bool> g_ovLButton {false}, g_ovRButton {false};
|
|
static std::atomic<int> g_ovWheelRaw {0};
|
|
static std::atomic<bool> g_ovWantMouse {false};
|
|
static std::atomic<bool> g_ovVisible {false};
|
|
|
|
// Map texture to show in the in-game window (set from C# each frame) + UV rect.
|
|
static std::atomic<void*> g_ovMapTex {nullptr};
|
|
static std::atomic<float> g_ovMapU0 {0.f}, g_ovMapV0 {1.f};
|
|
static std::atomic<float> g_ovMapU1 {1.f}, g_ovMapV1 {0.f};
|
|
// SRV over the map texture for ImGui::Image. Rebuilt each frame; must outlive
|
|
// RenderDrawData, so it lives at file scope rather than as a frame-local.
|
|
static ComPtr<ID3D11ShaderResourceView> g_ovMapSRV;
|
|
|
|
// Size (px) of the map image region this frame. C# reads it back to set the map
|
|
// camera's aspect to the window shape, so the map fills the window (no letterbox).
|
|
static std::atomic<float> g_ovViewW {0.f}, g_ovViewH {0.f};
|
|
void Overlay_GetMapView(float* outW, float* outH) {
|
|
if (outW) *outW = g_ovViewW.load();
|
|
if (outH) *outH = g_ovViewH.load();
|
|
}
|
|
|
|
void Overlay_SetAlpha(float alpha) { g_ovAlpha.store(std::max(0.1f, std::min(1.0f, alpha))); }
|
|
void Overlay_SetMapAlpha(float alpha) { g_mapAlpha.store(std::max(0.0f, std::min(1.0f, alpha))); }
|
|
void Overlay_SetMapBgAlpha(float alpha) { g_mapBgAlpha.store(std::max(0.0f, std::min(1.0f, alpha))); }
|
|
|
|
// Map-image interaction (drag/zoom) queued here for C# to forward to the map
|
|
// camera. Same InputEvent contract the popout uses, so C# can share the logic.
|
|
static InputQueue g_ovInput;
|
|
int Overlay_PollInput(InputEvent* out, int maxEvents) { return g_ovInput.drain(out, maxEvents); }
|
|
|
|
void Overlay_SetDeviceTexture(void* texturePtr) { g_ovDeviceTex.store(texturePtr); }
|
|
void Overlay_SetVisible(bool visible) { g_ovVisible.store(visible); }
|
|
bool Overlay_WantsMouse() { return g_ovWantMouse.load(); }
|
|
|
|
void Overlay_SetMapTexture(void* texturePtr, float u0, float v0, float u1, float v1) {
|
|
g_ovMapTex.store(texturePtr);
|
|
g_ovMapU0.store(u0); g_ovMapV0.store(v0);
|
|
g_ovMapU1.store(u1); g_ovMapV1.store(v1);
|
|
}
|
|
|
|
void Overlay_SetInput(float displayW, float displayH,
|
|
float mouseX, float mouseY,
|
|
bool lButton, bool rButton, int wheel) {
|
|
g_ovW.store(displayW); g_ovH.store(displayH);
|
|
g_ovMouseX.store(mouseX); g_ovMouseY.store(mouseY);
|
|
g_ovLButton.store(lButton); g_ovRButton.store(rButton);
|
|
if (wheel) g_ovWheelRaw.fetch_add(wheel);
|
|
}
|
|
|
|
void Renderer_PresentOverlay() {
|
|
if (!g_ovVisible.load()) return;
|
|
|
|
// Lazy device init — same path the popout uses, but from a throwaway texture
|
|
// since the overlay has no map RT of its own. Renderer_OnDeviceInit also runs
|
|
// InitImGui (shared context + DX11 backend), so g_imguiInited is set here too.
|
|
if (!g_device) {
|
|
void* t = g_ovDeviceTex.load();
|
|
if (!t) return;
|
|
auto* tex = static_cast<ID3D11Texture2D*>(t);
|
|
ID3D11Device* dev = nullptr;
|
|
tex->GetDevice(&dev);
|
|
if (!dev) return;
|
|
Renderer_OnDeviceInit(dev);
|
|
dev->Release();
|
|
}
|
|
if (!g_device || !g_context || !g_imguiInited) return;
|
|
|
|
float w = g_ovW.load(), h = g_ovH.load();
|
|
if (w <= 0.f || h <= 0.f) return;
|
|
|
|
// Save Unity's render state — we draw into its currently bound RTV and leave
|
|
// it bound (no OMSetRenderTargets), so ImGui composites onto the backbuffer.
|
|
D3D11StateBlock saved;
|
|
saved.Capture(g_context);
|
|
|
|
MapThemeData theme = SnapshotTheme();
|
|
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
io.DisplaySize = ImVec2(w, h);
|
|
io.MousePos = ImVec2(g_ovMouseX.load(), g_ovMouseY.load());
|
|
io.MouseDown[0] = g_ovLButton.load();
|
|
io.MouseDown[1] = g_ovRButton.load();
|
|
io.MouseWheel = (float)g_ovWheelRaw.exchange(0) / WHEEL_DELTA;
|
|
|
|
ImGui_ImplDX11_NewFrame();
|
|
ImGui::NewFrame();
|
|
|
|
// Chrome (window bg, title bar, toolbar, compass) alpha. Pushed before Begin()
|
|
// so all decorations are affected; popped around the map Image() so the map
|
|
// image has its own independent alpha, then popped finally after BuildMapUI.
|
|
const float ovAlpha = g_ovAlpha.load();
|
|
const float mapAlpha = g_mapAlpha.load();
|
|
const float mapBgA = g_mapBgAlpha.load();
|
|
// While the mouse is over the overlay, clamp chrome alpha to 50% so the UI
|
|
// remains readable even when the user has set a very low window opacity.
|
|
// Uses last frame's WantCaptureMouse (one-frame lag; imperceptible in practice).
|
|
const bool mouseOver = g_ovWantMouse.load();
|
|
const float effectiveAlpha = mouseOver ? std::max(ovAlpha, 0.5f) : ovAlpha;
|
|
const bool hasAlpha = effectiveAlpha < 0.999f;
|
|
if (hasAlpha)
|
|
ImGui::PushStyleVar(ImGuiStyleVar_Alpha, effectiveAlpha);
|
|
|
|
g_ovMapSRV.Reset(); // release last frame's view before building this frame's
|
|
|
|
// Captured inside the window, used to position the shared chrome (toolbar +
|
|
// compass) over the map image after the window's End().
|
|
ImVec2 mapScreenPos(0.f, 0.f), mapSize(0.f, 0.f);
|
|
bool mapShown = false;
|
|
|
|
// Shared UI-state holder (status, lists, rotation) + command queue for chrome.
|
|
PopoutWindow* stateWin = GetPopoutWindow(GetOrCreateOverlayStateHandle());
|
|
|
|
// Zero padding so the map fills the window edge-to-edge and the toolbar sits
|
|
// flush at the bottom (acting as the border) with no dark frame around it.
|
|
// WindowPadding is only read during Begin() so we pop it right after — this
|
|
// keeps it off the style stack so we can freely pop/push ovAlpha around Image().
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0.f, 0.f));
|
|
ImGui::SetNextWindowPos(ImVec2(80.f, 80.f), ImGuiCond_FirstUseEver);
|
|
ImGui::SetNextWindowSize(ImVec2(720.f, 480.f), ImGuiCond_FirstUseEver);
|
|
// Scale window bg alpha by mapBgA so the map background slider makes the void
|
|
// behind the camera transparent (showing Unity's scene), not the window chrome.
|
|
if (mapBgA < 0.999f)
|
|
ImGui::PushStyleColor(ImGuiCol_WindowBg,
|
|
ImVec4(theme.wBgR, theme.wBgG, theme.wBgB, theme.wBgA * mapBgA));
|
|
// p_open = true: ImGui adds an [X] close button to the title bar. When the user
|
|
// clicks it ImGui sets windowOpen=false; we push UICmd::Close so C# can call
|
|
// HideIfVisible() and DeactivateMap(). NoBringToFrontOnFocus keeps this window
|
|
// behind the chrome windows (toolbar + compass) that BuildMapUI creates after it.
|
|
bool windowOpen = true;
|
|
bool windowVisible = ImGui::Begin("Railroader Map##s3ingame", &windowOpen,
|
|
ImGuiWindowFlags_NoBringToFrontOnFocus);
|
|
// WindowPadding is read; pop now so ovAlpha is the only active style var.
|
|
ImGui::PopStyleVar();
|
|
if (mapBgA < 0.999f) ImGui::PopStyleColor();
|
|
|
|
if (windowVisible) {
|
|
void* mapTex = g_ovMapTex.load();
|
|
ImVec2 avail = ImGui::GetContentRegionAvail();
|
|
|
|
if (mapTex && avail.x > 4.f && avail.y > 4.f) {
|
|
auto* tex = static_cast<ID3D11Texture2D*>(mapTex);
|
|
D3D11_TEXTURE2D_DESC td; tex->GetDesc(&td);
|
|
|
|
D3D11_SHADER_RESOURCE_VIEW_DESC sd = {};
|
|
sd.Format = TypedFormat(td.Format);
|
|
sd.ViewDimension = D3D11_SRV_DIMENSION_TEXTURE2D;
|
|
sd.Texture2D.MostDetailedMip = 0;
|
|
sd.Texture2D.MipLevels = 1;
|
|
if (SUCCEEDED(g_device->CreateShaderResourceView(tex, &sd, &g_ovMapSRV))) {
|
|
// Fill the whole content region (no letterbox). C# reads this size
|
|
// back and sets camera.aspect to match, so the map renders the right
|
|
// slice to fill the window instead of leaving bars top/bottom.
|
|
ImVec2 sz = avail;
|
|
g_ovViewW.store(sz.x);
|
|
g_ovViewH.store(sz.y);
|
|
|
|
// Transparent hit area over the map for drag/zoom. Queue events in
|
|
// normalized [0,1] image space (top-left origin) so C# can forward
|
|
// them to the map camera with the same math the popout uses.
|
|
ImVec2 imgPos = ImGui::GetCursorScreenPos();
|
|
ImGui::InvisibleButton("##mapHit", sz, ImGuiButtonFlags_MouseButtonLeft);
|
|
bool hov = ImGui::IsItemHovered();
|
|
ImVec2 nrm = { (io.MousePos.x - imgPos.x) / sz.x,
|
|
(io.MousePos.y - imgPos.y) / sz.y };
|
|
auto pushOv = [&](int type, float d) {
|
|
InputEvent e{}; e.type = type; e.x = nrm.x; e.y = nrm.y; e.delta = d;
|
|
g_ovInput.push(e);
|
|
};
|
|
if (ImGui::IsItemActivated()) pushOv(LButtonDown, 0.f);
|
|
if (ImGui::IsItemActive() &&
|
|
(io.MouseDelta.x != 0.f || io.MouseDelta.y != 0.f))
|
|
pushOv(MouseMove, 0.f);
|
|
if (ImGui::IsItemDeactivated()) pushOv(LButtonUp, 0.f);
|
|
if (hov && io.MouseWheel != 0.f) pushOv(MouseWheel, io.MouseWheel);
|
|
if (hov && ImGui::IsMouseReleased(ImGuiMouseButton_Right)) pushOv(RButtonUp, 0.f);
|
|
|
|
// Draw the map over the same rect the hit-button occupies.
|
|
// Map alpha is independent of chrome alpha: pop ovAlpha so
|
|
// ImGuiStyleVar_Alpha doesn't multiply into the tint, then push back.
|
|
ImGui::SetCursorScreenPos(imgPos);
|
|
ImVec2 uv0(g_ovMapU0.load(), g_ovMapV0.load());
|
|
ImVec2 uv1(g_ovMapU1.load(), g_ovMapV1.load());
|
|
ImVec4 tint(theme.mapR, theme.mapG, theme.mapB, theme.mapA * mapAlpha);
|
|
if (hasAlpha) ImGui::PopStyleVar(); // lift chrome alpha
|
|
ImGui::Image((ImTextureID)g_ovMapSRV.Get(), sz, uv0, uv1, tint);
|
|
if (hasAlpha) ImGui::PushStyleVar(ImGuiStyleVar_Alpha, effectiveAlpha);
|
|
|
|
// Visible resize-grip indicator. ImGui draws its own grip during
|
|
// Begin() — behind our opaque map image, so invisible. We draw one
|
|
// on top in the corner only while the window is focused (the dark
|
|
// unfocused version was more distracting than no grip at all).
|
|
if (ImGui::IsWindowFocused()) {
|
|
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
ImVec2 br(imgPos.x + sz.x, imgPos.y + sz.y);
|
|
const float gripSz = 16.f;
|
|
ImU32 col = ImGui::GetColorU32(ImGuiCol_TitleBgActive, 1.0f);
|
|
dl->AddTriangleFilled(ImVec2(br.x, br.y),
|
|
ImVec2(br.x - gripSz, br.y),
|
|
ImVec2(br.x, br.y - gripSz), col);
|
|
}
|
|
|
|
mapScreenPos = imgPos; mapSize = sz; mapShown = true;
|
|
}
|
|
} else {
|
|
ImGui::TextWrapped("Load a save and open the in-game map, then it will "
|
|
"appear here. (F10 toggles this window.)");
|
|
}
|
|
}
|
|
ImGui::End();
|
|
// WindowPadding was already popped right after Begin(); nothing extra to pop here.
|
|
|
|
// If the user clicked [X], push a Close command for C# to process.
|
|
if (!windowOpen && stateWin) {
|
|
InputEvent ev{}; ev.type = UICommand;
|
|
ev.x = static_cast<float>(UICmd::Close);
|
|
stateWin->inputQueue.push(ev);
|
|
}
|
|
|
|
// Shared map chrome (toolbar + compass), positioned over the map image so the
|
|
// in-game map and the popout present the same UI. Reads the shared state holder.
|
|
if (mapShown && stateWin)
|
|
BuildMapUI(stateWin, mapScreenPos, mapSize.x, mapSize.y,
|
|
/*reserveResizeGrip=*/true, /*showPopOutBtn=*/true, theme, mapAlpha);
|
|
|
|
// Pop the global alpha AFTER all chrome windows, before Render().
|
|
if (hasAlpha)
|
|
ImGui::PopStyleVar();
|
|
|
|
ImGui::Render();
|
|
|
|
// Bind a linear (non-sRGB) view of Unity's backbuffer so the GPU does not
|
|
// sRGB-encode our writes (which washes the map/UI out). This makes the overlay
|
|
// match the popout, which renders into its own UNORM swapchain. Falls back to
|
|
// Unity's bound RTV if a UNORM view can't be created (no regression).
|
|
ComPtr<ID3D11RenderTargetView> linearRtv;
|
|
if (saved.rtv[0]) {
|
|
ComPtr<ID3D11Resource> res;
|
|
saved.rtv[0]->GetResource(&res);
|
|
ComPtr<ID3D11Texture2D> bb;
|
|
if (res && SUCCEEDED(res.As(&bb))) {
|
|
D3D11_TEXTURE2D_DESC bbDesc; bb->GetDesc(&bbDesc);
|
|
DXGI_FORMAT linear = UnormRtvFormat(bbDesc.Format);
|
|
if (linear != DXGI_FORMAT_UNKNOWN) {
|
|
D3D11_RENDER_TARGET_VIEW_DESC rd = {};
|
|
rd.Format = linear;
|
|
rd.ViewDimension = (bbDesc.SampleDesc.Count > 1)
|
|
? D3D11_RTV_DIMENSION_TEXTURE2DMS : D3D11_RTV_DIMENSION_TEXTURE2D;
|
|
g_device->CreateRenderTargetView(bb.Get(), &rd, &linearRtv);
|
|
}
|
|
}
|
|
}
|
|
if (linearRtv) {
|
|
ID3D11RenderTargetView* rtv = linearRtv.Get();
|
|
g_context->OMSetRenderTargets(1, &rtv, nullptr);
|
|
}
|
|
|
|
ImGui_ImplDX11_RenderDrawData(ImGui::GetDrawData());
|
|
g_ovWantMouse.store(io.WantCaptureMouse);
|
|
|
|
// Restore Unity's render state
|
|
saved.Restore(g_context);
|
|
}
|