MSCO 2.9.1: fixes without MSCLoader, admin teleport, MSCO branding
- Own JSON reader (MiniJson) instead of Newtonsoft.Json - the server browser failed with "Could not load System.Runtime.Serialization" once MSCLoader was gone - Mouse cursor visible in admin menu/chat/console (PlayerInMenu) - Intro "Press any key" screen replaced by an MSCO loading screen; hook PlayMakerFSM.Awake too - Join links while the game runs: picked up from MSCO\join.txt (written by the launcher) - Admin menu: TELEPORT tab (home, shop, repair shop, inspection, villages, events, Satsuma) - Window title/icon "MSCO - My Summer Car Online", MSCO logo in the main menu - In-game UI restyled like the keymaster website (rounded cards, pills, glow, slim scrollbars) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+182
@@ -0,0 +1,182 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
|
||||
namespace MSCO
|
||||
{
|
||||
/// <summary>
|
||||
/// MSCO statt "My Summer Car": Fenstertitel und -symbol, und im Hauptmenü das große Spiel-Logo durch das MSCO-Logo
|
||||
/// (branding/menu-logo.png, in die DLL eingebettet) ersetzen.
|
||||
/// </summary>
|
||||
public static class MenuBranding
|
||||
{
|
||||
const string WindowTitle = "MSCO - My Summer Car Online";
|
||||
const string MenuLogoResource = "MSCO.menu-logo.png";
|
||||
|
||||
// ---------------------------------------------------------------- Fenster
|
||||
|
||||
delegate bool EnumWindowsProc(IntPtr hwnd, IntPtr lParam);
|
||||
[DllImport("user32.dll")] static extern bool EnumWindows(EnumWindowsProc callback, IntPtr lParam);
|
||||
[DllImport("user32.dll")] static extern uint GetWindowThreadProcessId(IntPtr hwnd, out uint processId);
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern int GetClassNameW(IntPtr hwnd, StringBuilder name, int max);
|
||||
[DllImport("user32.dll", CharSet = CharSet.Unicode)] static extern bool SetWindowTextW(IntPtr hwnd, string text);
|
||||
[DllImport("user32.dll")] static extern IntPtr SendMessageW(IntPtr hwnd, uint msg, IntPtr wParam, IntPtr lParam);
|
||||
[DllImport("user32.dll")] static extern IntPtr CreateIconFromResourceEx(byte[] data, uint size, bool icon, uint version, int cx, int cy, uint flags);
|
||||
|
||||
static readonly EnumWindowsProc enumProc = OnWindow; // Delegate festhalten (sonst räumt der GC ihn weg)
|
||||
static uint processId;
|
||||
static IntPtr bigIcon, smallIcon;
|
||||
|
||||
/// <summary>Titel und Symbol des Spielfensters auf MSCO setzen (beim Start und nach jedem Szenenwechsel).</summary>
|
||||
public static void ApplyWindow()
|
||||
{
|
||||
try
|
||||
{
|
||||
processId = (uint)Process.GetCurrentProcess().Id;
|
||||
if (bigIcon == IntPtr.Zero && Branding.LogoPng.Length > 0)
|
||||
{
|
||||
// PNG-Daten direkt als Symbol (geht seit Windows Vista)
|
||||
bigIcon = CreateIconFromResourceEx(Branding.LogoPng, (uint)Branding.LogoPng.Length, true, 0x00030000, 0, 0, 0);
|
||||
smallIcon = CreateIconFromResourceEx(Branding.LogoPng, (uint)Branding.LogoPng.Length, true, 0x00030000, 16, 16, 0);
|
||||
}
|
||||
EnumWindows(enumProc, IntPtr.Zero);
|
||||
}
|
||||
catch (Exception e) { Bootstrap.Log("Window title/icon: " + e.Message); }
|
||||
}
|
||||
|
||||
static bool OnWindow(IntPtr hwnd, IntPtr lParam)
|
||||
{
|
||||
uint pid;
|
||||
GetWindowThreadProcessId(hwnd, out pid);
|
||||
if (pid != processId) return true;
|
||||
var name = new StringBuilder(64);
|
||||
GetClassNameW(hwnd, name, name.Capacity);
|
||||
if (name.ToString() != "UnityWndClass") return true;
|
||||
SetWindowTextW(hwnd, WindowTitle);
|
||||
const uint WM_SETICON = 0x0080;
|
||||
if (bigIcon != IntPtr.Zero) SendMessageW(hwnd, WM_SETICON, (IntPtr)1, bigIcon);
|
||||
if (smallIcon != IntPtr.Zero) SendMessageW(hwnd, WM_SETICON, IntPtr.Zero, smallIcon);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Logo im Hauptmenü
|
||||
|
||||
static Texture2D source;
|
||||
static readonly Dictionary<string, Texture2D> fitted = new Dictionary<string, Texture2D>();
|
||||
static bool dumped;
|
||||
|
||||
static Texture2D Source
|
||||
{
|
||||
get
|
||||
{
|
||||
if (source != null) return source;
|
||||
using (Stream s = Assembly.GetExecutingAssembly().GetManifestResourceStream(MenuLogoResource))
|
||||
{
|
||||
if (s == null) return null;
|
||||
var data = new byte[s.Length];
|
||||
int read = 0;
|
||||
while (read < data.Length) read += s.Read(data, read, data.Length - read);
|
||||
source = new Texture2D(2, 2, TextureFormat.ARGB32, false);
|
||||
source.LoadImage(data);
|
||||
source.hideFlags = HideFlags.HideAndDontSave;
|
||||
}
|
||||
return source;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Ist das eine Textur des Spiel-Logos? (Name enthält "logo", aber nicht vom Auto/Laden ...)</summary>
|
||||
static bool IsGameLogo(Texture t)
|
||||
{
|
||||
if (t == null || t == source) return false;
|
||||
string n = t.name.ToLowerInvariant();
|
||||
return n.Contains("logo") && !n.Contains("msco") && !n.Contains("satsuma") && !n.Contains("teimo") && !n.Contains("amistech");
|
||||
}
|
||||
|
||||
/// <summary>Im Hauptmenü das Spiel-Logo ersetzen. Gibt die Anzahl ersetzter Texturen zurück.</summary>
|
||||
public static int ReplaceMenuLogo()
|
||||
{
|
||||
if (Source == null) return 0;
|
||||
int replaced = 0;
|
||||
var seen = new List<string>();
|
||||
// Auch ausgeblendete Objekte (das Menü wechselt zwischen mehreren Logo-Objekten)
|
||||
foreach (Renderer r in Resources.FindObjectsOfTypeAll<Renderer>())
|
||||
{
|
||||
if (r == null || r.gameObject.hideFlags != HideFlags.None) continue;
|
||||
Material[] materials = r.sharedMaterials;
|
||||
for (int i = 0; i < materials.Length; i++)
|
||||
{
|
||||
Material m = materials[i];
|
||||
if (m == null || m.mainTexture == null) continue;
|
||||
if (!dumped && seen.Count < 200) seen.Add(Path(r.transform) + " : " + m.mainTexture.name);
|
||||
if (!IsGameLogo(m.mainTexture)) continue;
|
||||
Texture original = m.mainTexture;
|
||||
m.mainTexture = Fit(original.width, original.height);
|
||||
Bootstrap.Log("Menu logo replaced: " + Path(r.transform) + " (" + original.name + ", " + original.width + "x" + original.height + ")");
|
||||
replaced++;
|
||||
}
|
||||
}
|
||||
foreach (SpriteRenderer sr in UnityEngine.Object.FindObjectsOfType<SpriteRenderer>())
|
||||
{
|
||||
if (sr == null || sr.sprite == null || !IsGameLogo(sr.sprite.texture)) continue;
|
||||
Texture2D tex = Fit(sr.sprite.texture.width, sr.sprite.texture.height);
|
||||
sr.sprite = Sprite.Create(tex, new Rect(0, 0, tex.width, tex.height), new Vector2(0.5f, 0.5f), sr.sprite.pixelsPerUnit);
|
||||
Bootstrap.Log("Menu logo (sprite) replaced: " + Path(sr.transform));
|
||||
replaced++;
|
||||
}
|
||||
foreach (GUITexture g in UnityEngine.Object.FindObjectsOfType<GUITexture>())
|
||||
{
|
||||
if (g == null || !IsGameLogo(g.texture)) continue;
|
||||
g.texture = Fit(g.texture.width, g.texture.height);
|
||||
Bootstrap.Log("Menu logo (GUITexture) replaced: " + Path(g.transform));
|
||||
replaced++;
|
||||
}
|
||||
if (!dumped && replaced == 0)
|
||||
{
|
||||
// Zur Diagnose: welche Texturen gibt es im Menü? (nur einmal)
|
||||
Bootstrap.Log("Menu logo not found. Textures in the menu:\n " + string.Join("\n ", seen.ToArray()));
|
||||
}
|
||||
dumped = true;
|
||||
return replaced;
|
||||
}
|
||||
|
||||
/// <summary>MSCO-Logo mittig in eine Textur der Größe des Originals (gleiches Seitenverhältnis wie dessen Fläche).</summary>
|
||||
static Texture2D Fit(int width, int height)
|
||||
{
|
||||
width = Mathf.Clamp(width, 64, 2048);
|
||||
height = Mathf.Clamp(height, 64, 2048);
|
||||
string key = width + "x" + height;
|
||||
Texture2D tex;
|
||||
if (fitted.TryGetValue(key, out tex) && tex != null) return tex;
|
||||
|
||||
tex = new Texture2D(width, height, TextureFormat.ARGB32, true);
|
||||
tex.hideFlags = HideFlags.HideAndDontSave;
|
||||
tex.wrapMode = TextureWrapMode.Clamp;
|
||||
float scale = Mathf.Min((float)width / Source.width, (float)height / Source.height);
|
||||
float w = Source.width * scale, h = Source.height * scale;
|
||||
float x0 = (width - w) / 2f, y0 = (height - h) / 2f;
|
||||
var pixels = new Color[width * height];
|
||||
for (int y = 0; y < height; y++)
|
||||
for (int x = 0; x < width; x++)
|
||||
{
|
||||
float u = (x - x0) / w, v = (y - y0) / h;
|
||||
pixels[y * width + x] = u < 0f || u > 1f || v < 0f || v > 1f ? new Color(0, 0, 0, 0) : Source.GetPixelBilinear(u, v);
|
||||
}
|
||||
tex.SetPixels(pixels);
|
||||
tex.Apply(true);
|
||||
fitted[key] = tex;
|
||||
return tex;
|
||||
}
|
||||
|
||||
static string Path(Transform t)
|
||||
{
|
||||
string path = t.name;
|
||||
for (Transform p = t.parent; p != null; p = p.parent) path = p.name + "/" + path;
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user