mirror of
https://git.crafterpika.cc/crafterpika/SplatTool-public.git
synced 2026-08-17 10:42:15 +02:00
initial commit
This commit is contained in:
parent
64b4a24e61
commit
3ab62216aa
31 changed files with 2790 additions and 1 deletions
74
.forgejo/workflows/publish-release.yml
Normal file
74
.forgejo/workflows/publish-release.yml
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
name: Build new release
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
Build:
|
||||
runs-on: debian-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Install Dependencies
|
||||
run: |
|
||||
apt update
|
||||
apt install curl p7zip nsis -y
|
||||
|
||||
- name: Setup dotnet 8 SDK
|
||||
run: curl -sSL https://dot.net/v1/dotnet-install.sh | bash /dev/stdin --channel 8.0
|
||||
|
||||
- name: Build for Windows/Linux
|
||||
run: |
|
||||
export DOTNET_ROOT=$HOME/.dotnet
|
||||
export PATH=$PATH:$HOME/.dotnet:$HOME/.dotnet/tools
|
||||
export DOTNET_CLI_TELEMETRY_OPTOUT=true
|
||||
|
||||
echo "VERSION_TAG=$(cat VERSION.txt)" >> "$FORGEJO_ENV"
|
||||
echo "SHORT_SHA=$(git rev-parse --short HEAD)" >> "$FORGEJO_ENV"
|
||||
|
||||
dotnet tool install --global Obfuscar.GlobalTool
|
||||
dotnet publish -r win-x64 --self-contained true -p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true
|
||||
dotnet publish -r linux-x64 --self-contained true -p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true
|
||||
|
||||
mkdir pkg-linux
|
||||
mkdir pkg-win
|
||||
cp -r modmenu pkg-linux/
|
||||
cp -r modmenu pkg-win/
|
||||
cp icon.ico pkg-linux/
|
||||
cp icon.ico pkg-win/
|
||||
cp assets/config.json pkg-linux/
|
||||
cp assets/config.json pkg-win/
|
||||
cp bin/Release/net8.0/linux-x64/publish/SplatTool pkg-linux/SplatTool
|
||||
cp bin/Release/net8.0/linux-x64/publish/Photino.Native.so pkg-linux/Photino.Native.so
|
||||
cp bin/Release/net8.0/linux-x64/publish/libnfd.so pkg-linux/libnfd.so
|
||||
cp bin/Release/net8.0/win-x64/publish/SplatTool.exe pkg-win/SplatTool.exe
|
||||
cp bin/Release/net8.0/win-x64/publish/Photino.Native.dll pkg-win/Photino.Native.dll
|
||||
cp bin/Release/net8.0/win-x64/publish/nfd.dll pkg-win/nfd.dll
|
||||
cp bin/Release/net8.0/win-x64/publish/WebView2Loader.dll pkg-win/WebView2Loader.dll
|
||||
|
||||
- name: Package up for Release
|
||||
run: |
|
||||
cd pkg-linux
|
||||
tar -cvzf ../SplatTool-${{ env.VERSION_TAG }}-${{ env.SHORT_SHA }}-x86_64-linux.tar.gz * && cd ..
|
||||
cd pkg-win
|
||||
7z a ../SplatTool-${{ env.VERSION_TAG }}-${{ env.SHORT_SHA }}-x86_64-win.7z * && cd ..
|
||||
makensis "-DVERSION=${{ env.VERSION_TAG }}" "-DSHORT_SHA=${{ env.SHORT_SHA }}" ./SplatTool_Installer.nsi
|
||||
|
||||
- name: Upload Windows Installer
|
||||
uses: forgejo/upload-artifact@v4
|
||||
with:
|
||||
name: SplatTool-Win-Installer
|
||||
path: SplatTool-${{ env.VERSION_TAG }}-${{ env.SHORT_SHA }}-Setup.exe
|
||||
|
||||
- name: Upload Windows Portable
|
||||
uses: forgejo/upload-artifact@v4
|
||||
with:
|
||||
name: SplatTool-Win-Portable
|
||||
path: SplatTool-${{ env.VERSION_TAG }}-${{ env.SHORT_SHA }}-x86_64-win.7z
|
||||
|
||||
- name: Upload Linux Build
|
||||
uses: forgejo/upload-artifact@v4
|
||||
with:
|
||||
name: SplatTool-Linux
|
||||
path: SplatTool-${{ env.VERSION_TAG }}-${{ env.SHORT_SHA }}-x86_64-linux.tar.gz
|
||||
8
.gitignore
vendored
Normal file
8
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
bin/*
|
||||
obj/*
|
||||
obj/Debug/*
|
||||
obj/Release/*
|
||||
.DS_Store
|
||||
dist/*
|
||||
*.dmg
|
||||
*.sln
|
||||
12
.vscode/settings.json
vendored
Normal file
12
.vscode/settings.json
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
{
|
||||
"omnisharp.enableEditorConfigSupport": false,
|
||||
"omnisharp.useModernNet": false,
|
||||
"csharp.format.enable": false,
|
||||
"[csharp]": {
|
||||
"editor.formatOnSave": false,
|
||||
"editor.formatOnType": false,
|
||||
"editor.formatOnPaste": false
|
||||
},
|
||||
"dotnet.formatting.organizeImportsOnFormat": false,
|
||||
"editor.useEditorConfig": false
|
||||
}
|
||||
86
FreezeThreads.cs
Normal file
86
FreezeThreads.cs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
using ModMenu;
|
||||
using Func;
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace FreezeThread {
|
||||
public class WorkThread {
|
||||
private Thread MainThread;
|
||||
private bool running = false;
|
||||
public enum ThreadStatus { Running, Paused, Stopped }
|
||||
private ThreadStatus status = ThreadStatus.Stopped;
|
||||
private readonly object statusLock = new object();
|
||||
|
||||
public ThreadStatus Status {
|
||||
get {
|
||||
lock (statusLock) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
private set {
|
||||
lock (statusLock) {
|
||||
status = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private readonly ManualResetEventSlim pauseEvent = new ManualResetEventSlim(true);
|
||||
private readonly Action ThreadAction;
|
||||
|
||||
public WorkThread(Action ThreadAction) {
|
||||
this.ThreadAction = ThreadAction
|
||||
?? throw new ArgumentNullException(nameof(ThreadAction));
|
||||
}
|
||||
|
||||
private void ThreadLoop() {
|
||||
while (running) {
|
||||
try {
|
||||
pauseEvent.Wait();
|
||||
|
||||
if (pauseEvent.IsSet)
|
||||
Status = ThreadStatus.Running;
|
||||
|
||||
ThreadAction();
|
||||
Thread.Sleep(1); // Prevent CPU Exhaustion by adding small delay
|
||||
} catch (ArgumentOutOfRangeException ex) {
|
||||
running = false;
|
||||
Status = ThreadStatus.Stopped;
|
||||
Console.WriteLine($"Thread Killed! Caught: {ex}");
|
||||
}
|
||||
}
|
||||
|
||||
Status = ThreadStatus.Stopped;
|
||||
}
|
||||
|
||||
public void Start() {
|
||||
if (running)
|
||||
return;
|
||||
|
||||
MainThread = new Thread(ThreadLoop);
|
||||
MainThread.IsBackground = true;
|
||||
running = true;
|
||||
MainThread.Start();
|
||||
}
|
||||
|
||||
public void Pause() {
|
||||
if (Status == ThreadStatus.Running) {
|
||||
pauseEvent.Reset();
|
||||
Status = ThreadStatus.Paused;
|
||||
}
|
||||
}
|
||||
|
||||
public void Resume() {
|
||||
if (Status == ThreadStatus.Paused) {
|
||||
pauseEvent.Set();
|
||||
Status = ThreadStatus.Running;
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop() {
|
||||
running = false;
|
||||
pauseEvent.Set();
|
||||
MainThread?.Join();
|
||||
Status = ThreadStatus.Stopped;
|
||||
}
|
||||
}
|
||||
}
|
||||
204
Func.cs
Normal file
204
Func.cs
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
// Modules
|
||||
using ModMenu;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
using System.Text;
|
||||
using System.Drawing;
|
||||
using DrawingColor = System.Drawing.Color;
|
||||
using System.Net;
|
||||
using System.Dynamic;
|
||||
using System.Text.Json;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats.Tga;
|
||||
using NativeFileDialogSharp;
|
||||
|
||||
namespace Func {
|
||||
public class Player {
|
||||
public string nnidHex { get; set; }
|
||||
public uint PID { get; set; }
|
||||
public string NNID { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
||||
|
||||
public class SessionData {
|
||||
public uint SessionID { get; set; }
|
||||
public List<Player> Players { get; set; } = new List<Player>();
|
||||
}
|
||||
|
||||
public class ExtFunc {
|
||||
public static async Task<string> GetNNID(int pid, bool isPretendo = true) {
|
||||
using (HttpClient client = new HttpClient()) {
|
||||
client.DefaultRequestHeaders.Add("User-Agent", "Mozilla/5.0");
|
||||
// Thanks @tombun2 for pointing this out
|
||||
// src: https://github.com/Milk-Cool/PretendoLookup/blob/932263e2c5e7f2e7527ff5dc690a8701dbf39885/miidata.js
|
||||
client.DefaultRequestHeaders.Add("X-Nintendo-Client-ID", "a2efa818a34fa16b8afbc8a74eba3eda");
|
||||
client.DefaultRequestHeaders.Add("X-Nintendo-Client-Secret", "c91cdb5658bd4954ade78533a339cf9a");
|
||||
|
||||
try {
|
||||
// src: https://github.com/kinnay/NintendoClients/wiki/Account-Server
|
||||
if (isPretendo) {
|
||||
HttpResponseMessage response = await client.GetAsync($"http://account.pretendo.cc/v1/api/miis?pids={pid}");
|
||||
if (response.IsSuccessStatusCode) {
|
||||
string content = await response.Content.ReadAsStringAsync();
|
||||
XDocument doc = XDocument.Parse(content);
|
||||
string userId = doc.Root?.Element("mii")?.Element("user_id")?.Value;
|
||||
return userId;
|
||||
} else {
|
||||
return "0";
|
||||
}
|
||||
} else {
|
||||
HttpResponseMessage response = await client.GetAsync($"https://account.spfn.net/v1/api/admin/mapped_ids?input_type=pid&output_type=user_id&input={pid}");
|
||||
if (response.IsSuccessStatusCode) {
|
||||
string content = await response.Content.ReadAsStringAsync();
|
||||
XDocument doc = XDocument.Parse(content);
|
||||
string userId = doc.Root?.Element("mapped_id")?.Element("out_id")?.Value;
|
||||
return userId;
|
||||
} else {
|
||||
return "0";
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex) {
|
||||
Console.WriteLine(ex.Message);
|
||||
return "0";
|
||||
}
|
||||
}
|
||||
}
|
||||
public static int FindPattern(byte[] data, byte[] pattern) {
|
||||
for (int i = 0; i <= data.Length - pattern.Length; i++) {
|
||||
bool match = true;
|
||||
for (int j = 0; j < pattern.Length; j++) {
|
||||
if (data[i + j] != pattern[j]) {
|
||||
match = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (match)
|
||||
return i;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
}
|
||||
|
||||
public class Codes {
|
||||
public static void DisconnectFromLobby() {
|
||||
Int16 timer = 0;
|
||||
var client_time_ptr = Program.readUInt32((uint)(Program.readUInt32((uint)(Program.readUInt32((uint)(Program.readUInt32(0x101E5660) + 0x28)) + 0x24)) + 0x40));
|
||||
Program.writeInt16((uint)(client_time_ptr + 0x1F8), timer);
|
||||
}
|
||||
public static void NameChanger(string name) {
|
||||
Console.WriteLine($"Changing Name to: {name}...");
|
||||
var name_pointer = Program.readUInt32(0x101E80A4);
|
||||
Program.writeBytes((uint)(name_pointer + 0x8C), new byte[32]);
|
||||
Program.writeBytes((uint)(name_pointer + 0x8C), Encoding.BigEndianUnicode.GetBytes(name));
|
||||
}
|
||||
public static string GetSessionData() {
|
||||
bool isPretendo = true;
|
||||
bool isSpfnTried = false;
|
||||
string nnidStr = null;
|
||||
httpMenu.session.SessionID = 0;
|
||||
httpMenu.session.Players.Clear();
|
||||
httpMenu.session = new SessionData();
|
||||
|
||||
for (var i = 0; i < 8; i++) {
|
||||
var ptrToPlayerInfo = Program.readUInt32((uint)(Program.readUInt32((uint)(Program.readUInt32(0x101DD330) + 0x10)) + (uint)(i * 4))); // PlayerInfo Pointer
|
||||
var name = Encoding.BigEndianUnicode.GetString(Program.readBytes(ptrToPlayerInfo + 0x6, 32)).Replace("\n", "").Replace("\r", "").TrimEnd('\0');
|
||||
var pidRaw = Program.readUInt32(ptrToPlayerInfo + 0xd0);
|
||||
if (pidRaw != 0) {
|
||||
nnidStr = ExtFunc.GetNNID((int)pidRaw, isPretendo).GetAwaiter().GetResult();
|
||||
} else {
|
||||
nnidStr = "0";
|
||||
}
|
||||
if (nnidStr == "0" && !isSpfnTried && pidRaw != 0) {
|
||||
nnidStr = ExtFunc.GetNNID((int)pidRaw, false).GetAwaiter().GetResult();
|
||||
if (nnidStr != "0") {
|
||||
isPretendo = false;
|
||||
} else {
|
||||
isSpfnTried = true;
|
||||
}
|
||||
}
|
||||
string nnidHex = BitConverter.ToString(BitConverter.GetBytes(pidRaw)).Replace("-", "");
|
||||
|
||||
httpMenu.session.Players.Add(new Player {
|
||||
nnidHex = nnidHex,
|
||||
PID = pidRaw,
|
||||
NNID = nnidStr,
|
||||
Name = name
|
||||
});
|
||||
}
|
||||
var ptr = Program.readUInt32(0x101E8980);
|
||||
uint sessionID;
|
||||
if (ptr != 0) {
|
||||
var index = Program.readBytes(ptr + 0xBD, 1)[0];
|
||||
sessionID = Program.readUInt32(ptr + index + 0xCC);
|
||||
} else {
|
||||
sessionID = 0;
|
||||
}
|
||||
httpMenu.session.SessionID = sessionID;
|
||||
|
||||
string json = JsonSerializer.Serialize(httpMenu.session, new JsonSerializerOptions { WriteIndented = true });
|
||||
return json;
|
||||
}
|
||||
public static void forceSessiondId(uint sessionId) {
|
||||
// Console.WriteLine(sessionId);
|
||||
var p = Program.readUInt32(0x101DD330);
|
||||
Program.writeUInt32((uint)(p + 0x260), (uint)sessionId);
|
||||
}
|
||||
public static void faceimgtga(int format) {
|
||||
var p = Program.readUInt32(0x101DCDB0) + 0x150;
|
||||
var localPlayer = Program.readBytes(p, 5376);
|
||||
byte[] pattern = new byte[]{ 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x80, 0x00 };
|
||||
int faceimg_offset = ExtFunc.FindPattern(localPlayer, pattern);
|
||||
if (faceimg_offset == -1) {
|
||||
Console.WriteLine("Could not find FaceImg.tga pattern!");
|
||||
} else {
|
||||
var faceimg = Program.readBytes((uint)(p + faceimg_offset), 65580);
|
||||
string path = null;
|
||||
string defaultPath = null;
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
|
||||
defaultPath = Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments);
|
||||
} else {
|
||||
defaultPath = Path.GetDirectoryName(Environment.ProcessPath);
|
||||
}
|
||||
|
||||
if (format == 0) {
|
||||
path = Dialog.FileSave("tga", defaultPath).Path;
|
||||
if (string.IsNullOrEmpty(path)) return;
|
||||
File.WriteAllBytes(path, faceimg);
|
||||
} else if (format == 1) {
|
||||
path = Dialog.FileSave("png", defaultPath).Path;
|
||||
if (string.IsNullOrEmpty(path)) return;
|
||||
using (Image image = Image.Load(faceimg)) {
|
||||
image.SaveAsPng(path);
|
||||
}
|
||||
}
|
||||
if (string.IsNullOrEmpty(path)) return;
|
||||
// on *nix systems set file to be owned by everyone.
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux) || RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
|
||||
File.SetUnixFileMode(path, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute | UnixFileMode.GroupRead | UnixFileMode.GroupWrite | UnixFileMode.GroupExecute | UnixFileMode.OtherRead | UnixFileMode.OtherWrite | UnixFileMode.OtherExecute);
|
||||
}
|
||||
}
|
||||
}
|
||||
public static string TotalWins() {
|
||||
try {
|
||||
var p_SaveDataMgr = Program.readUInt32((uint)(Program.readUInt32(SaveEditor.SAVEDATAMGR_ROOT_PTR) + 0xC));
|
||||
return $"{Program.readUInt32((uint)(p_SaveDataMgr + 0xCB38))}";
|
||||
} catch {
|
||||
return "Game not fully loaded yet...";
|
||||
}
|
||||
}
|
||||
public static string TotalLoss() {
|
||||
try {
|
||||
var p_SaveDataMgr = Program.readUInt32((uint)(Program.readUInt32(SaveEditor.SAVEDATAMGR_ROOT_PTR) + 0xC));
|
||||
return $"{Program.readUInt32((uint)(p_SaveDataMgr + 0xCB3C))}";
|
||||
} catch {
|
||||
return "Game not fully loaded yet...";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
420
ProcessWrapper.cs
Normal file
420
ProcessWrapper.cs
Normal file
|
|
@ -0,0 +1,420 @@
|
|||
// modules
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace ProcessWrapper {
|
||||
public interface IProcessWrapper {
|
||||
byte[] ReadProcessMemory(int pid, ulong address, uint length);
|
||||
bool WriteProcessMemory(int pid, ulong address, byte[] data);
|
||||
int getuid();
|
||||
ulong FindCemuBase(int pid, ulong minSize);
|
||||
}
|
||||
|
||||
// Mac implementation
|
||||
public class MacProcessWrapper : IProcessWrapper {
|
||||
// vars
|
||||
const int KERN_SUCCESS = 0;
|
||||
const string LIBSYSTEM = "/usr/lib/libSystem.B.dylib";
|
||||
const int VM_REGION_BASIC_INFO_64 = 9;
|
||||
const int VM_REGION_BASIC_INFO_COUNT_64 = 10;
|
||||
const int VM_PROT_READ = 0x01;
|
||||
|
||||
// Struct
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct vm_region_basic_info_64 {
|
||||
public int protection;
|
||||
public int max_protection;
|
||||
public int inheritance;
|
||||
public int shared;
|
||||
public int reserved;
|
||||
public ulong offset;
|
||||
public int behavior;
|
||||
public short user_wired_count;
|
||||
public short user_tag;
|
||||
}
|
||||
|
||||
// https://github.com/attilathedud/macos_task_for_pid#overview
|
||||
// Well documenated apis, thanks Apple.
|
||||
[DllImport(LIBSYSTEM, SetLastError = true)]
|
||||
private static extern int task_for_pid(IntPtr task,
|
||||
int pid,
|
||||
out IntPtr targetTask);
|
||||
|
||||
[DllImport(LIBSYSTEM, SetLastError = true)]
|
||||
private static extern IntPtr mach_task_self();
|
||||
|
||||
[DllImport(LIBSYSTEM, SetLastError = true)]
|
||||
private static extern int mach_vm_read(IntPtr target_task,
|
||||
ulong address,
|
||||
ulong size,
|
||||
out IntPtr data,
|
||||
out ulong data_count);
|
||||
|
||||
[DllImport(LIBSYSTEM, SetLastError = true)]
|
||||
private static extern int mach_vm_write(IntPtr target_task,
|
||||
ulong address,
|
||||
byte[] data,
|
||||
uint dataCnt);
|
||||
|
||||
[DllImport(LIBSYSTEM, SetLastError = true)]
|
||||
private static extern int vm_deallocate(IntPtr task,
|
||||
IntPtr address,
|
||||
ulong size);
|
||||
|
||||
[DllImport(LIBSYSTEM, SetLastError = true)]
|
||||
private static extern int mach_vm_region(IntPtr task,
|
||||
ref ulong address,
|
||||
out ulong size,
|
||||
int flavor,
|
||||
IntPtr info,
|
||||
ref uint count,
|
||||
out ulong object_name);
|
||||
|
||||
[DllImport(LIBSYSTEM, SetLastError = true)]
|
||||
private static extern int geteuid();
|
||||
|
||||
public int getuid() {
|
||||
return geteuid();
|
||||
}
|
||||
|
||||
public byte[] ReadProcessMemory(int pid, ulong address, uint length) {
|
||||
IntPtr task;
|
||||
IntPtr localTask = mach_task_self();
|
||||
|
||||
int result = task_for_pid(localTask, pid, out task);
|
||||
if (result != KERN_SUCCESS) {
|
||||
Console.WriteLine($"task_for_pid failed with code {result}");
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
IntPtr bufferPtr;
|
||||
ulong size;
|
||||
result = mach_vm_read(task, address, length, out bufferPtr, out size);
|
||||
if (result != KERN_SUCCESS) {
|
||||
//Console.WriteLine($"mach_vm_read failed with code {result}");
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
byte[] buffer = new byte[size];
|
||||
Marshal.Copy(bufferPtr, buffer, 0, (int)size);
|
||||
|
||||
// Clean up
|
||||
vm_deallocate(localTask, bufferPtr, size);
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public bool WriteProcessMemory(int pid, ulong address, byte[] data) {
|
||||
IntPtr task;
|
||||
IntPtr localTask = mach_task_self();
|
||||
|
||||
int result = task_for_pid(localTask, pid, out task);
|
||||
if (result != KERN_SUCCESS) {
|
||||
//throw new Exception($"task_for_pid failed: {result}");
|
||||
return false;
|
||||
}
|
||||
|
||||
result = mach_vm_write(task, address, data, (uint)data.Length);
|
||||
if (result != KERN_SUCCESS) {
|
||||
//throw new Exception($"mach_vm_write failed: {result}");
|
||||
Console.WriteLine($"mach_vm_write failed: {result}");
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Console.WriteLine($"Wrote {data.Length} bytes to 0x{address:X}");
|
||||
}
|
||||
|
||||
public ulong FindCemuBase(int pid, ulong maxSize) {
|
||||
IntPtr task;
|
||||
IntPtr localTask = mach_task_self();
|
||||
byte?[] patternBytes = new byte?[] { 0x02, 0xD4, 0xE7 };
|
||||
|
||||
int result = task_for_pid(localTask, pid, out task);
|
||||
if (result != KERN_SUCCESS) {
|
||||
Console.WriteLine($"task_for_pid failed: {result}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
ulong address = 0;
|
||||
while (true) {
|
||||
ulong size;
|
||||
uint count = VM_REGION_BASIC_INFO_COUNT_64;
|
||||
IntPtr infoPtr = Marshal.AllocHGlobal(Marshal.SizeOf<vm_region_basic_info_64>());
|
||||
ulong objectName;
|
||||
|
||||
result = mach_vm_region(task,
|
||||
ref address,
|
||||
out size,
|
||||
VM_REGION_BASIC_INFO_64,
|
||||
infoPtr,
|
||||
ref count,
|
||||
out objectName
|
||||
);
|
||||
|
||||
if (result != KERN_SUCCESS) {
|
||||
Marshal.FreeHGlobal(infoPtr);
|
||||
break;
|
||||
}
|
||||
|
||||
var regionInfo = Marshal.PtrToStructure<vm_region_basic_info_64>(infoPtr);
|
||||
Marshal.FreeHGlobal(infoPtr);
|
||||
|
||||
bool readable = (regionInfo.protection & VM_PROT_READ) != 0;
|
||||
|
||||
if (readable && size < maxSize || size == maxSize) {
|
||||
var bytes = ReadProcessMemory(pid, (ulong)(address + 0xE000000), 20);
|
||||
|
||||
if (bytes != null && bytes.Length > 0) {
|
||||
int patternLen = patternBytes.Length;
|
||||
for (int i = 0, j = 0; i + patternLen <= 20; i++) {
|
||||
if (patternBytes[j] == null || bytes[i] == patternBytes[j]) {
|
||||
j++;
|
||||
} else {
|
||||
j = 0;
|
||||
}
|
||||
|
||||
if (j >= patternLen) {
|
||||
return address;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
address += size;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Linux implementation
|
||||
public class LinuxProcessWrapper : IProcessWrapper {
|
||||
// IOVec Struct
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct IOVec {
|
||||
public UIntPtr Base;
|
||||
public UIntPtr Length;
|
||||
}
|
||||
|
||||
[DllImport("libc", SetLastError = true)]
|
||||
private static extern long process_vm_readv(int pid,
|
||||
IOVec[] local_iov, ulong liovcnt,
|
||||
IOVec[] remote_iov, ulong riovcnt,
|
||||
ulong flags);
|
||||
|
||||
[DllImport("libc", SetLastError = true)]
|
||||
private static extern long process_vm_writev(int pid,
|
||||
IOVec[] local_iov, ulong liovcnt,
|
||||
IOVec[] remote_iov, ulong riovcnt,
|
||||
ulong flags);
|
||||
|
||||
[DllImport("libc", SetLastError = true)]
|
||||
private static extern int geteuid();
|
||||
|
||||
public int getuid() {
|
||||
return geteuid();
|
||||
}
|
||||
|
||||
public byte[] ReadProcessMemory(int pid, ulong address, uint length) {
|
||||
byte[] buffer = new byte[length];
|
||||
GCHandle handle = GCHandle.Alloc(buffer, GCHandleType.Pinned);
|
||||
|
||||
try {
|
||||
var local = new IOVec {
|
||||
Base = new UIntPtr((ulong)handle.AddrOfPinnedObject().ToInt64()),
|
||||
Length = (UIntPtr)length
|
||||
};
|
||||
|
||||
var remote = new IOVec {
|
||||
Base = (UIntPtr)address,
|
||||
Length = (UIntPtr)length
|
||||
};
|
||||
|
||||
long nread = process_vm_readv(pid, new[] { local }, 1, new[] { remote }, 1, 0);
|
||||
if (nread == -1) {
|
||||
// Console.WriteLine($"Failed to read memory. Error code: {Marshal.GetLastWin32Error()}");
|
||||
return new byte[0];
|
||||
} else {
|
||||
// Console.WriteLine($"Read {nread} bytes from process {pid} at 0x{address:X}");
|
||||
}
|
||||
} finally {
|
||||
handle.Free();
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
public bool WriteProcessMemory(int pid, ulong address, byte[] data) {
|
||||
GCHandle handle = GCHandle.Alloc(data, GCHandleType.Pinned);
|
||||
|
||||
try {
|
||||
var local = new IOVec {
|
||||
Base = new UIntPtr((ulong)handle.AddrOfPinnedObject().ToInt64()),
|
||||
Length = (UIntPtr)data.Length
|
||||
};
|
||||
|
||||
var remote = new IOVec {
|
||||
Base = (UIntPtr)address,
|
||||
Length = (UIntPtr)data.Length
|
||||
};
|
||||
|
||||
long nwritten = process_vm_writev(pid, new[] { local }, 1, new[] { remote }, 1, 0);
|
||||
if (nwritten == -1) {
|
||||
// Console.WriteLine($"Failed to write memory. Error code: {Marshal.GetLastWin32Error()}");
|
||||
return false;
|
||||
} else {
|
||||
// Console.WriteLine($"Wrote {nwritten} bytes to process {pid} at 0x{address:X}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
finally {
|
||||
handle.Free();
|
||||
}
|
||||
}
|
||||
|
||||
public ulong FindCemuBase(int pid, ulong maxSize) {
|
||||
// vars
|
||||
string mapsPath = $"/proc/{pid}/maps";
|
||||
byte?[] patternBytes = new byte?[] { 0x02, 0xD4, 0xE7 };
|
||||
|
||||
string[] mapsLines = File.ReadAllLines(mapsPath);
|
||||
foreach (string line in mapsLines) {
|
||||
var split = line.Split(" ");
|
||||
var addrP = split[0].Split("-");
|
||||
ulong min = Convert.ToUInt64(addrP[0], 16);
|
||||
ulong max = Convert.ToUInt64(addrP[1], 16);
|
||||
|
||||
if (!split[1].Contains("r")) continue;
|
||||
var len = (max - min);
|
||||
if (len > maxSize || len == maxSize) {
|
||||
var bytes = ReadProcessMemory(pid, (ulong)(min + 0xE000000), 20);
|
||||
|
||||
if (bytes != null && bytes.Length > 0) {
|
||||
int patternLen = patternBytes.Length;
|
||||
for (int i = 0, j = 0; i + patternLen <= 20; i++) {
|
||||
if (patternBytes[j] == null || bytes[i] == patternBytes[j]) {
|
||||
j++;
|
||||
} else {
|
||||
j = 0;
|
||||
}
|
||||
|
||||
if (j >= patternLen) {
|
||||
return min;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
public class WindowsProcessWrapper : IProcessWrapper {
|
||||
// vars
|
||||
const uint PROCESS_VM_READ = 0x0010;
|
||||
const uint PROCESS_VM_WRITE = 0x0020;
|
||||
const uint PROCESS_VM_OPERATION = 0x0008;
|
||||
const uint PROCESS_QUERY_INFORMATION = 0x0400;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct MEMORY_BASIC_INFORMATION {
|
||||
public IntPtr BaseAddress;
|
||||
public IntPtr AllocationBase;
|
||||
public uint AllocationProtect;
|
||||
public IntPtr RegionSize;
|
||||
public uint State;
|
||||
public uint Protect;
|
||||
public uint Type;
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern IntPtr OpenProcess(uint dwDesiredAccess,
|
||||
bool bInheritHandle,
|
||||
int dwProcessId);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool ReadProcessMemory(IntPtr hProcess,
|
||||
IntPtr lpBaseAddress,
|
||||
[Out] byte[] lpBuffer,
|
||||
int dwSize,
|
||||
out int lpNumberOfBytesRead);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern bool WriteProcessMemory(IntPtr hProcess,
|
||||
IntPtr lpBaseAddress,
|
||||
byte[] lpBuffer,
|
||||
int dwSize,
|
||||
out int lpNumberOfBytesWritten);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern int VirtualQueryEx(IntPtr hProcess,
|
||||
IntPtr lpAddress,
|
||||
out MEMORY_BASIC_INFORMATION lpBuffer,
|
||||
uint dwLength);
|
||||
|
||||
// Windows doesnt need this.
|
||||
public int getuid() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
public byte[] ReadProcessMemory(int pid, ulong address, uint length) {
|
||||
IntPtr hProcess = OpenProcess(PROCESS_VM_READ | PROCESS_QUERY_INFORMATION, false, pid);
|
||||
if (hProcess == IntPtr.Zero) {
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
byte[] buffer = new byte[length];
|
||||
if (ReadProcessMemory(hProcess, (IntPtr)address, buffer, (int)length, out int bytesRead) && bytesRead > 0) {
|
||||
if (bytesRead < length) {
|
||||
Array.Resize(ref buffer, bytesRead);
|
||||
}
|
||||
return buffer;
|
||||
}
|
||||
|
||||
return new byte[0];
|
||||
}
|
||||
|
||||
public bool WriteProcessMemory(int pid, ulong address, byte[] data) {
|
||||
IntPtr hProcess = OpenProcess(PROCESS_VM_WRITE | PROCESS_VM_OPERATION, false, pid);
|
||||
if (hProcess == IntPtr.Zero) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return WriteProcessMemory(hProcess, (IntPtr)address, data, data.Length, out int bytesWritten) && bytesWritten == data.Length;
|
||||
}
|
||||
|
||||
public ulong FindCemuBase(int pid, ulong maxSize) {
|
||||
byte?[] patternBytes = new byte?[] { 0x02, 0xD4, 0xE7 };
|
||||
IntPtr address = IntPtr.Zero;
|
||||
MEMORY_BASIC_INFORMATION mbi;
|
||||
|
||||
IntPtr hProcess = OpenProcess(PROCESS_VM_WRITE | PROCESS_VM_OPERATION, false, pid);
|
||||
if (hProcess == IntPtr.Zero) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
while (VirtualQueryEx(hProcess, address, out mbi, (uint)Marshal.SizeOf(typeof(MEMORY_BASIC_INFORMATION))) != 0) {
|
||||
bool readable = (mbi.State == 0x1000) && (mbi.Protect != 0x01);
|
||||
ulong regionSize = (ulong)mbi.RegionSize.ToInt64();
|
||||
if (readable && regionSize > maxSize || regionSize == maxSize) {
|
||||
var bytes = ReadProcessMemory(pid, (ulong)(mbi.BaseAddress + 0xE000000), 20);
|
||||
if (bytes != null && bytes.Length > 0) {
|
||||
int patternLen = patternBytes.Length;
|
||||
for (int i = 0, j = 0; i + patternLen <= 20; i++) {
|
||||
if (patternBytes[j] == null || bytes[i] == patternBytes[j]) {
|
||||
j++;
|
||||
} else {
|
||||
j = 0;
|
||||
}
|
||||
if (j >= patternLen) {
|
||||
return (ulong)mbi.BaseAddress;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
address = new IntPtr(mbi.BaseAddress.ToInt64() + mbi.RegionSize.ToInt64());
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
253
Program.cs
Normal file
253
Program.cs
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
using ProcessWrapper;
|
||||
using Func;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.IO;
|
||||
using Photino.NET;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace ModMenu {
|
||||
public class Program {
|
||||
public static int pid = 0;
|
||||
public static int OsType = 0;
|
||||
public static IProcessWrapper OsProcessWrapper = null;
|
||||
public static ulong cbase = 0;
|
||||
public static ulong cbaseCorrected = 0;
|
||||
public static Dictionary<string, string> config = null;
|
||||
|
||||
public static byte[] readBytes(uint address, uint length) {
|
||||
return OsProcessWrapper.ReadProcessMemory(pid, (ulong)(cbase + 0xE000000) + address - 0x10000000, length);
|
||||
}
|
||||
|
||||
public static uint readUInt32(uint address) {
|
||||
return BitConverter.ToUInt32(OsProcessWrapper.ReadProcessMemory(pid, (ulong)(cbase + 0xE000000) + address - 0x10000000, 4).Reverse().ToArray(), 0);
|
||||
}
|
||||
|
||||
public static float readFloat(uint address) {
|
||||
var b = OsProcessWrapper.ReadProcessMemory(pid, (ulong)(cbase + 0xE000000) + address - 0x10000000, 4);
|
||||
Array.Reverse(b, 0, b.Length);
|
||||
return BitConverter.ToSingle(b, 0);
|
||||
}
|
||||
|
||||
public static void writeBytes(uint address, byte[] bytes) {
|
||||
OsProcessWrapper.WriteProcessMemory(pid, (ulong)(cbase + 0xE000000) + address - 0x10000000, bytes);
|
||||
}
|
||||
|
||||
public static void writeUInt32(uint address, uint value) {
|
||||
byte[] b = BitConverter.GetBytes(value);
|
||||
Array.Reverse(b, 0, b.Length);
|
||||
OsProcessWrapper.WriteProcessMemory(pid, (ulong)(cbase + 0xE000000) + address - 0x10000000, b);
|
||||
}
|
||||
|
||||
public static void writeInt16(uint address, Int16 value) {
|
||||
byte[] b = BitConverter.GetBytes(value);
|
||||
Array.Reverse(b, 0, b.Length);
|
||||
OsProcessWrapper.WriteProcessMemory(pid, (ulong)(cbase + 0xE000000) + address - 0x10000000, b);
|
||||
}
|
||||
|
||||
public static void writeFloat(uint address, float value) {
|
||||
byte[] b = BitConverter.GetBytes(value);
|
||||
Array.Reverse(b, 0, b.Length);
|
||||
OsProcessWrapper.WriteProcessMemory(pid, (ulong)(cbase + 0xE000000) + address - 0x10000000, b);
|
||||
}
|
||||
|
||||
public static string FindExecutable(string name) {
|
||||
// Not required for Mac/Windows
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) return null;
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) return null;
|
||||
|
||||
var pathEnv = Environment.GetEnvironmentVariable("PATH");
|
||||
if (string.IsNullOrEmpty(pathEnv)) return null;
|
||||
var paths = pathEnv.Split(':');
|
||||
|
||||
foreach (var path in paths) {
|
||||
var fullPath = Path.Combine(path, name);
|
||||
if (File.Exists(fullPath)) {
|
||||
return fullPath;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public static bool IsWayland() {
|
||||
string runtimeDir = Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR");
|
||||
if (string.IsNullOrEmpty(runtimeDir)) return false;
|
||||
|
||||
return File.Exists(Path.Combine(runtimeDir, "wayland-0"));
|
||||
}
|
||||
|
||||
[STAThread]
|
||||
public static void Main(string[] args) {
|
||||
Console.OutputEncoding = System.Text.Encoding.UTF8;
|
||||
|
||||
if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux)) {
|
||||
OsType = 1;
|
||||
OsProcessWrapper = new LinuxProcessWrapper();
|
||||
if (OsProcessWrapper.getuid() != 0) {
|
||||
var pkexecPath = FindExecutable("pkexec");
|
||||
|
||||
if (!string.IsNullOrEmpty(pkexecPath)) {
|
||||
string exePath = Process.GetCurrentProcess().MainModule.FileName;
|
||||
string display;
|
||||
string runtimeDir;
|
||||
string command = null;
|
||||
|
||||
Console.WriteLine($"Wayland is: {IsWayland()}");
|
||||
|
||||
if (IsWayland()) {
|
||||
display = Environment.GetEnvironmentVariable("WAYLAND_DISPLAY");
|
||||
runtimeDir = Environment.GetEnvironmentVariable("XDG_RUNTIME_DIR");
|
||||
command = $"--disable-internal-agent env XDG_RUNTIME_DIR={runtimeDir} WAYLAND_DISPLAY={display} \"{exePath}\"";
|
||||
} else {
|
||||
display = Environment.GetEnvironmentVariable("DISPLAY");
|
||||
command = $"--disable-internal-agent env DISPLAY={display} \"{exePath}\"";
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(command)) {
|
||||
Console.WriteLine("Please run as root...");
|
||||
System.Environment.Exit(1);
|
||||
}
|
||||
|
||||
var startInfo = new ProcessStartInfo {
|
||||
FileName = pkexecPath,
|
||||
Arguments = command,
|
||||
UseShellExecute = false
|
||||
};
|
||||
|
||||
try {
|
||||
using (Process elevatedProcess = Process.Start(startInfo)) {
|
||||
elevatedProcess?.WaitForExit();
|
||||
Environment.Exit(elevatedProcess?.ExitCode ?? 0);
|
||||
};
|
||||
} catch (Exception ex) {
|
||||
Console.WriteLine($"Elevation failed: {ex.Message}");
|
||||
Environment.Exit(1);
|
||||
}
|
||||
} else {
|
||||
Console.WriteLine("Please run as root...");
|
||||
System.Environment.Exit(1);
|
||||
}
|
||||
}
|
||||
} else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX)) {
|
||||
OsType = 2;
|
||||
OsProcessWrapper = new MacProcessWrapper();
|
||||
if (OsProcessWrapper.getuid() != 0) {
|
||||
string basePath = AppContext.BaseDirectory;
|
||||
string rootHelperPath = Path.Combine(basePath, "root_helper");
|
||||
|
||||
if (Path.Exists(rootHelperPath)) {
|
||||
var startInfo = new ProcessStartInfo {
|
||||
FileName = rootHelperPath,
|
||||
UseShellExecute = false
|
||||
};
|
||||
|
||||
try {
|
||||
Process.Start(startInfo);
|
||||
Environment.Exit(0);
|
||||
} catch (Exception ex) {
|
||||
Console.WriteLine($"Elevation failed: {ex.Message}");
|
||||
Environment.Exit(1);
|
||||
}
|
||||
} else {
|
||||
Console.WriteLine("Please run as root...");
|
||||
System.Environment.Exit(1);
|
||||
}
|
||||
}
|
||||
} else if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) {
|
||||
OsType = 3;
|
||||
OsProcessWrapper = new WindowsProcessWrapper();
|
||||
} else {
|
||||
Console.WriteLine("Unsupported Operating System exiting...");
|
||||
return;
|
||||
}
|
||||
|
||||
// Root access needed past here for Linux/Mac
|
||||
string[] targetNames = { "cemu", "xapfish", ".cemu-wrapped", "cemu_release" };
|
||||
Process targetProcess = Process.GetProcesses()
|
||||
.FirstOrDefault(p => targetNames.Any(name => p.ProcessName.Equals(name, StringComparison.OrdinalIgnoreCase)));
|
||||
|
||||
if (targetProcess == null) {
|
||||
new PhotinoWindow()
|
||||
.SetSize(0, 0)
|
||||
.SetMinimized(true)
|
||||
.SetNotificationRegistrationId("8FDF1B15-3408-47A6-8EF5-2B0676B76277")
|
||||
.SetNotificationsEnabled(false)
|
||||
.RegisterWindowCreatedHandler((sender, e) => {
|
||||
var window = sender as PhotinoWindow;
|
||||
window.ShowMessage("Error", "Could not find Cemu process.", PhotinoDialogButtons.Ok, PhotinoDialogIcon.Error);
|
||||
window.Close();
|
||||
System.Environment.Exit(0);
|
||||
})
|
||||
.LoadRawString("<html></html>")
|
||||
.WaitForClose();
|
||||
return;
|
||||
}
|
||||
pid = targetProcess.Id;
|
||||
cbase = OsProcessWrapper.FindCemuBase(pid, 1308622848);
|
||||
Console.WriteLine($"Found target process: {targetProcess.ProcessName} (PID: {pid})");
|
||||
|
||||
if (cbase != 0) {
|
||||
Console.WriteLine("SplatTool v3.2.0 by CrafterPika");
|
||||
Console.WriteLine("Special Thanks: Winterberry, Javi.ig and Tombuntu, KittenTM.");
|
||||
|
||||
cbaseCorrected = (Program.cbase + 0xE000000) - 0x10000000;
|
||||
|
||||
string indexPath;
|
||||
string iconPath;
|
||||
string configPath;
|
||||
|
||||
if (OsType == 2) {
|
||||
indexPath = Path.Combine(AppContext.BaseDirectory, "..", "Resources", "modmenu", "index.html");
|
||||
iconPath = Path.Combine(AppContext.BaseDirectory, "..", "Resources", "icon.ico");
|
||||
configPath = Path.Combine(AppContext.BaseDirectory, "..", "Resources", "config.json");
|
||||
} else {
|
||||
indexPath = Path.Combine(AppContext.BaseDirectory, "modmenu", "index.html");
|
||||
iconPath = Path.Combine(AppContext.BaseDirectory, "icon.ico");
|
||||
configPath = Path.Combine(AppContext.BaseDirectory, "config.json");
|
||||
}
|
||||
|
||||
string configJson = File.ReadAllText(configPath);
|
||||
config = JsonSerializer.Deserialize<Dictionary<string, string>>(configJson);
|
||||
|
||||
var window = new PhotinoWindow()
|
||||
.SetTitle("SplatTool Mod Menu")
|
||||
.SetNotificationRegistrationId("8FDF1B15-3408-47A6-8EF5-2B0676B76277")
|
||||
.SetNotificationsEnabled(false)
|
||||
.SetUseOsDefaultSize(false)
|
||||
.SetSize(900, 700)
|
||||
.Center()
|
||||
.RegisterWebMessageReceivedHandler((object sender, string message) => {
|
||||
var w = sender as PhotinoWindow;
|
||||
var data = JsonSerializer.Deserialize<ModMenuRequest>(message);
|
||||
if (data != null) {
|
||||
string result = httpMenu.RunCode(data);
|
||||
w.SendWebMessage(result);
|
||||
}
|
||||
});
|
||||
|
||||
if (File.Exists(iconPath)) {
|
||||
window.SetIconFile(iconPath);
|
||||
}
|
||||
|
||||
window.Load(indexPath).WaitForClose();
|
||||
|
||||
} else {
|
||||
new PhotinoWindow()
|
||||
.SetSize(0, 0)
|
||||
.SetNotificationRegistrationId("8FDF1B15-3408-47A6-8EF5-2B0676B76277")
|
||||
.SetNotificationsEnabled(false)
|
||||
.SetMinimized(true)
|
||||
.RegisterWindowCreatedHandler((sender, e) => {
|
||||
var window = sender as PhotinoWindow;
|
||||
window.ShowMessage("Error", "Cemu base not found! Is Splatoon running?!", PhotinoDialogButtons.Ok, PhotinoDialogIcon.Error);
|
||||
window.Close();
|
||||
System.Environment.Exit(0);
|
||||
})
|
||||
.LoadRawString("<html></html>")
|
||||
.WaitForClose();
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
28
README.md
28
README.md
|
|
@ -1,2 +1,28 @@
|
|||
# SplatTool-public
|
||||
# SplatTool (Public ver.)
|
||||
|
||||
A cross-platform mod menu for Splatoon v288 on [Cemu](https://github.com/cemu-project/Cemu/releases) 2.6/2.7 (Experimental)<br>
|
||||
This version has been scrubbed of anything abusive.
|
||||
|
||||
<br>
|
||||
<small>SplatTool running on macOS 26.4.1</small>
|
||||
|
||||
|
||||
## Running
|
||||
|
||||
## Linux
|
||||
|
||||
Since Linux actually demands root priviliages to read another applications memory, you have to run it as root.<br>
|
||||
You can either run the binary directly with `sudo` or `doas` or launch it directly for pkexec popup.
|
||||
|
||||
Since the UI relies on [Photino](https://www.tryphotino.io/) it needs `webkit2gtk-4.1` installed.
|
||||
|
||||
On x11 you may need to run first `xhost +SI:localuser:root` to allow forwarding gtk to your window manager from root. Unsure if the same has to be done for wayland.
|
||||
|
||||
## MacOS
|
||||
|
||||
Same situation with Linux. The application bundles a small root helper program to assist with this.<br>
|
||||
But since i dont pay Apple 99$/year you first have to `xattr -cr /Applications/SplatTool.app` to bypass their stupid gatekeeper or do so in System Preferences -> Privacy & Security -> Open Anyway.
|
||||
|
||||
## Windows
|
||||
|
||||
Nothing special required to run.
|
||||
|
|
|
|||
178
SaveEditor.cs
Normal file
178
SaveEditor.cs
Normal file
|
|
@ -0,0 +1,178 @@
|
|||
using ModMenu;
|
||||
using Func;
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.Json;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
|
||||
namespace ModMenu {
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
public struct Gear {
|
||||
public uint GearID;
|
||||
public uint UnlockedSlots;
|
||||
public uint AvailableSlots;
|
||||
public uint Slot1;
|
||||
public uint Slot2;
|
||||
public uint Slot3;
|
||||
public uint Unk1; // Default: 0x00000000
|
||||
public uint Unk2; // Default: 0x00000000
|
||||
public uint Unk3; // Default: 0x00000000
|
||||
public uint TimestampLastUsed;
|
||||
public uint HideNewIcon; // Enabled: 0x00010000 | Disabled: 0x00000000
|
||||
public uint Unk4; // Default: 0x00000000
|
||||
}
|
||||
|
||||
public class GearQueueSlot {
|
||||
public string Class { get; set; }
|
||||
public uint Slot { get; set; }
|
||||
public byte[] Data { get; set; }
|
||||
}
|
||||
|
||||
public class SaveEditor {
|
||||
// [[0x106E975C] + 0xC] + 0x61A0 # Start of Headgear Definitions
|
||||
// [[0x106E975C] + 0xC] + 0x31A0 # Start of Clothes Definitions
|
||||
// [[0x106E975C] + 0xC] + 0x1A0 # Start of Shoes Definitions
|
||||
// Each Gear class is 0x3000 big and each indivdual gear is 0x30 bytes (48 Bytes)
|
||||
// 0x3000 / 0x30 = 0x100 Slots (256 Slots)
|
||||
// 48 Bytes are split evenly according to struct in order above.
|
||||
public static uint SAVEDATAMGR_ROOT_PTR = 0x101E675C; // Begin of Save Data [0x101E675C] + 0xC
|
||||
|
||||
public static string FetchAllGear() {
|
||||
var p_save_start = Program.readUInt32((uint)(Program.readUInt32(SAVEDATAMGR_ROOT_PTR) + 0xC));
|
||||
uint GearSectionSize = 0x3000;
|
||||
var results = new Dictionary<string, List<Gear>>();
|
||||
int structSize = Marshal.SizeOf<Gear>();
|
||||
|
||||
var categories = new (string Name, uint Offset)[] {
|
||||
("headgear", 0x61A0),
|
||||
("clothing", 0x31A0),
|
||||
("shoes", 0x1A0)
|
||||
};
|
||||
|
||||
foreach (var cat in categories) {
|
||||
var gearList = new List<Gear>();
|
||||
|
||||
byte[] sectionRaw = Program.readBytes(p_save_start + cat.Offset, GearSectionSize);
|
||||
|
||||
for (int i = 0; i < GearSectionSize; i += structSize) {
|
||||
byte[] structBuffer = new byte[structSize];
|
||||
Buffer.BlockCopy(sectionRaw, i, structBuffer, 0, structSize);
|
||||
|
||||
for (int j = 0; j < structBuffer.Length; j += 4) {
|
||||
Array.Reverse(structBuffer, j, 4);
|
||||
}
|
||||
Gear item = ByteArrayToStructure<Gear>(structBuffer);
|
||||
|
||||
gearList.Add(item);
|
||||
}
|
||||
results.Add(cat.Name, gearList);
|
||||
}
|
||||
|
||||
return JsonSerializer.Serialize(results, new JsonSerializerOptions {
|
||||
WriteIndented = true,
|
||||
IncludeFields = true
|
||||
});
|
||||
}
|
||||
|
||||
public static void QueueGearSlot(uint Slot, string Class, uint GearID, uint Slot1, uint Slot2, uint Slot3) {
|
||||
if (Slot > 255) {
|
||||
Console.WriteLine("You can only write to Slot 0-255...");
|
||||
return;
|
||||
}
|
||||
|
||||
Gear newGear = new Gear {
|
||||
GearID = GearID,
|
||||
UnlockedSlots = 4,
|
||||
AvailableSlots = 4,
|
||||
Slot1 = Slot1,
|
||||
Slot2 = Slot2,
|
||||
Slot3 = Slot3,
|
||||
Unk1 = 0,
|
||||
Unk2 = 0,
|
||||
Unk3 = 0,
|
||||
TimestampLastUsed = (uint)DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
||||
HideNewIcon = 0x00010000,
|
||||
Unk4 = 0
|
||||
};
|
||||
|
||||
byte[] rawData = StructureToByteArray(newGear);
|
||||
for (int i = 0; i < rawData.Length; i += 4) {
|
||||
Array.Reverse(rawData, i, 4);
|
||||
}
|
||||
|
||||
httpMenu.GearQueue.Add(new GearQueueSlot { Class = Class, Slot = Slot, Data = rawData });
|
||||
}
|
||||
|
||||
public static void WriteGearQueue() {
|
||||
var p_save_start = Program.readUInt32((uint)(Program.readUInt32(SAVEDATAMGR_ROOT_PTR) + 0xC));
|
||||
uint start_offset = 0;
|
||||
|
||||
foreach (var Gear in httpMenu.GearQueue.ToList()) {
|
||||
switch (Gear.Class.ToLower()) {
|
||||
case "headgear": start_offset = 0x61A0; break;
|
||||
case "clothing": start_offset = 0x31A0; break;
|
||||
case "shoes": start_offset = 0x1A0; break;
|
||||
default:
|
||||
Console.WriteLine("Invalid Class selected");
|
||||
return;
|
||||
}
|
||||
|
||||
Program.writeBytes((uint)(p_save_start + start_offset + (Gear.Slot * 0x30)), Gear.Data);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static void ChangeGender(uint gender) {
|
||||
if (gender > 2) {
|
||||
gender = 0;
|
||||
}
|
||||
var p_save_start = Program.readUInt32((uint)(Program.readUInt32(SAVEDATAMGR_ROOT_PTR) + 0xC));
|
||||
Program.writeUInt32((uint)(p_save_start + 0x190), gender);
|
||||
}
|
||||
|
||||
public static void ChangeLevel(uint level) {
|
||||
uint ActualLevel = level - 1;
|
||||
var p_save_start = Program.readUInt32((uint)(Program.readUInt32(SAVEDATAMGR_ROOT_PTR) + 0xC));
|
||||
Program.writeUInt32((uint)(p_save_start + 0xA5A8), ActualLevel);
|
||||
}
|
||||
|
||||
public static void ChangeCash(uint amount) {
|
||||
if (amount > 9999999) {
|
||||
amount = 9999999;
|
||||
}
|
||||
var p_save_start = Program.readUInt32((uint)(Program.readUInt32(SAVEDATAMGR_ROOT_PTR) + 0xC));
|
||||
Program.writeUInt32((uint)(p_save_start + 0xA5A0), amount);
|
||||
}
|
||||
|
||||
public static void ChangeSeaSnails(uint amount) {
|
||||
if (amount > 999) {
|
||||
amount = 999;
|
||||
}
|
||||
var p_save_start = Program.readUInt32((uint)(Program.readUInt32(SAVEDATAMGR_ROOT_PTR) + 0xC));
|
||||
Program.writeUInt32((uint)(p_save_start + 0xA5B4), amount);
|
||||
}
|
||||
|
||||
private static T ByteArrayToStructure<T>(byte[] bytes) where T : struct {
|
||||
GCHandle handle = GCHandle.Alloc(bytes, GCHandleType.Pinned);
|
||||
try {
|
||||
return Marshal.PtrToStructure<T>(handle.AddrOfPinnedObject());
|
||||
} finally {
|
||||
handle.Free();
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] StructureToByteArray(object obj) {
|
||||
int len = Marshal.SizeOf(obj);
|
||||
byte[] arr = new byte[len];
|
||||
IntPtr ptr = Marshal.AllocHGlobal(len);
|
||||
try {
|
||||
Marshal.StructureToPtr(obj, ptr, true);
|
||||
Marshal.Copy(ptr, arr, 0, len);
|
||||
} finally {
|
||||
Marshal.FreeHGlobal(ptr);
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
}
|
||||
}
|
||||
27
SplatTool.csproj
Normal file
27
SplatTool.csproj
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>disable</Nullable>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<SelfContained>true</SelfContained>
|
||||
<ApplicationIcon>icon.ico</ApplicationIcon>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="NativeFileDialogSharp" Version="0.6.0-alpha" />
|
||||
<PackageReference Include="Photino.NET" Version="4.0.16" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="3.1.12" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Update="modmenu\**\*">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="assets/config.json">
|
||||
<Link>config.json</Link>
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
3
SplatTool.slnx
Normal file
3
SplatTool.slnx
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<Solution>
|
||||
<Project Path="SplatTool.csproj" />
|
||||
</Solution>
|
||||
55
SplatTool_Installer.nsi
Normal file
55
SplatTool_Installer.nsi
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
!define PKG_DIR "pkg-win"
|
||||
|
||||
!ifndef VERSION
|
||||
!error "VERSION not defined (pass with -DVERSION=...)"
|
||||
!endif
|
||||
|
||||
!ifndef SHORT_SHA
|
||||
!error "SHORT_SHA not defined (pass with -DSHORT_SHA=...)"
|
||||
!endif
|
||||
|
||||
Name "SplatTool"
|
||||
OutFile "SplatTool-${VERSION}-${SHORT_SHA}-Setup.exe"
|
||||
InstallDir "$PROGRAMFILES64\SplatTool"
|
||||
RequestExecutionLevel admin ; Needed to write to Program Files
|
||||
|
||||
Page directory
|
||||
Page instfiles
|
||||
|
||||
Section "Install Files"
|
||||
|
||||
SetOutPath "$INSTDIR"
|
||||
|
||||
File /r "${PKG_DIR}\*.*"
|
||||
|
||||
; Create shortcuts
|
||||
CreateDirectory "$SMPROGRAMS\SplatTool"
|
||||
CreateShortCut "$DESKTOP\SplatTool.lnk" "$INSTDIR\SplatTool.exe" "" "$INSTDIR\icon.ico"
|
||||
CreateShortCut "$SMPROGRAMS\SplatTool\SplatTool.lnk" "$INSTDIR\SplatTool.exe" "" "$INSTDIR\icon.ico"
|
||||
|
||||
WriteUninstaller "$INSTDIR\Uninstall.exe"
|
||||
|
||||
; Register in Add/Remove Programs
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\SplatTool" "DisplayName" "SplatTool"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\SplatTool" "UninstallString" "$INSTDIR\Uninstall.exe"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\SplatTool" "DisplayIcon" "$INSTDIR\icon.ico"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\SplatTool" "DisplayVersion" "${VERSION}"
|
||||
WriteRegStr HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\SplatTool" "Publisher" "CrafterPika"
|
||||
|
||||
SectionEnd
|
||||
|
||||
Section "Uninstall"
|
||||
|
||||
Delete "$INSTDIR\SplatTool.exe"
|
||||
Delete "$INSTDIR\icon.ico"
|
||||
|
||||
RMDir /r "$INSTDIR"
|
||||
|
||||
Delete "$DESKTOP\SplatTool.lnk"
|
||||
Delete "$SMPROGRAMS\SplatTool\SplatTool.lnk"
|
||||
RMDir "$SMPROGRAMS\SplatTool"
|
||||
|
||||
; Remove registry keys on uninstall
|
||||
DeleteRegKey HKLM "Software\Microsoft\Windows\CurrentVersion\Uninstall\SplatTool"
|
||||
|
||||
SectionEnd
|
||||
137
TelemetryWebpost.cs
Normal file
137
TelemetryWebpost.cs
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
using ModMenu;
|
||||
using Func;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace TelemetryWebpost {
|
||||
public class SplatTelemetry {
|
||||
private static readonly HttpClient client = new HttpClient();
|
||||
public static uint LastStartNetworkTime = 0;
|
||||
|
||||
public static string Telemetry2Json(byte[] rawData) {
|
||||
if (rawData == null || rawData.Length == 0)
|
||||
return "{}";
|
||||
|
||||
int actualStart = Array.IndexOf(rawData, (byte)0x2D);
|
||||
if (actualStart == -1) return "{}";
|
||||
|
||||
string content = Encoding.UTF8.GetString(rawData, actualStart, rawData.Length - actualStart);
|
||||
|
||||
string[] lines = content.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None);
|
||||
if (lines.Length == 0 || string.IsNullOrWhiteSpace(lines[0]))
|
||||
return "{}";
|
||||
|
||||
string boundary = lines[0].Trim();
|
||||
|
||||
string[] parts = content.Split(new[] { boundary }, StringSplitOptions.RemoveEmptyEntries);
|
||||
var result = new Dictionary<string, string>();
|
||||
|
||||
foreach (var part in parts) {
|
||||
if (string.IsNullOrWhiteSpace(part) ||
|
||||
part.Trim() == "--" ||
|
||||
part.Contains("name=\"FaceImg\"") || // Kit Kat said he doesnt need faceimg.tga so i'll skip it :innocent:, makes my life easier.
|
||||
!part.Contains("Content-Disposition"))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var nameMatch = Regex.Match(part, "name=\"(?<name>[^\"]+)\"");
|
||||
if (!nameMatch.Success) continue;
|
||||
string key = nameMatch.Groups["name"].Value;
|
||||
|
||||
int valueStart = part.IndexOf("\r\n\r\n");
|
||||
int offset = 4;
|
||||
|
||||
if (valueStart == -1) {
|
||||
valueStart = part.IndexOf("\n\n");
|
||||
offset = 2;
|
||||
}
|
||||
|
||||
if (valueStart == -1) continue;
|
||||
|
||||
string value = part.Substring(valueStart + offset).Trim(new[] { '\r', '\n', '-', ' ' });
|
||||
|
||||
result[key] = value;
|
||||
}
|
||||
|
||||
var options = new JsonSerializerOptions {
|
||||
WriteIndented = true,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
|
||||
};
|
||||
|
||||
return JsonSerializer.Serialize(result, options);
|
||||
}
|
||||
|
||||
public static async Task SendTelemetryHttp(string targetUrl, string uniqueId, string TelemetryPayload, byte[] faceImgData) {
|
||||
var TelemetryPayloadDict = JsonSerializer.Deserialize<Dictionary<string, string>>(TelemetryPayload);
|
||||
TelemetryPayloadDict["ServerEnv"] = "L1";
|
||||
if (LastStartNetworkTime == Convert.ToUInt32(TelemetryPayloadDict["StartNetworkTime"])) {
|
||||
Console.WriteLine("Data sent already...");
|
||||
return;
|
||||
}
|
||||
LastStartNetworkTime = Convert.ToUInt32(TelemetryPayloadDict["StartNetworkTime"]);
|
||||
try {
|
||||
using (var content = new MultipartFormDataContent()) {
|
||||
foreach (var kvp in TelemetryPayloadDict) {
|
||||
content.Add(new StringContent(kvp.Value), kvp.Key);
|
||||
}
|
||||
|
||||
var imageContent = new ByteArrayContent(faceImgData);
|
||||
imageContent.Headers.ContentType = new System.Net.Http.Headers.MediaTypeHeaderValue("application/octet-stream");
|
||||
content.Add(imageContent, "FaceImg", "face.bin");
|
||||
|
||||
byte[] requestBody = await content.ReadAsByteArrayAsync();
|
||||
|
||||
string sha1Hash;
|
||||
using (SHA1 sha1 = SHA1.Create()) {
|
||||
byte[] hashBytes = sha1.ComputeHash(requestBody);
|
||||
sha1Hash = BitConverter.ToString(hashBytes).Replace("-", "").ToLower();
|
||||
}
|
||||
|
||||
var request = new HttpRequestMessage(HttpMethod.Post, targetUrl);
|
||||
request.Content = content;
|
||||
|
||||
request.Headers.Add("X-BOSS-Digest", sha1Hash);
|
||||
request.Headers.Add("X-BOSS-UniqueId", uniqueId);
|
||||
|
||||
Console.WriteLine($"Sending telemetry for {TelemetryPayloadDict["MiiName"]}...");
|
||||
HttpResponseMessage response = await client.SendAsync(request);
|
||||
|
||||
if (response.IsSuccessStatusCode) {
|
||||
Console.WriteLine("SUCCESS: Splatoon result saved.");
|
||||
} else {
|
||||
string errorText = await response.Content.ReadAsStringAsync();
|
||||
Console.WriteLine($"FAILED: {(int)response.StatusCode} - {errorText}");
|
||||
}
|
||||
}
|
||||
} catch (Exception ex) {
|
||||
Console.WriteLine($"Connection Error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
public static void SendTelemetry() {
|
||||
if (Program.readUInt32(0x101E4FF8) == 0) {
|
||||
var p = Program.readUInt32(0x101DCDB0) + 0x150;
|
||||
var data = Program.readBytes(p, 0x1800);
|
||||
|
||||
byte[] faceimg_pattern = new byte[]{ 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x80, 0x00 };
|
||||
int faceimg_offset = ExtFunc.FindPattern(data, faceimg_pattern);
|
||||
if (faceimg_offset == -1) {
|
||||
return;
|
||||
}
|
||||
var faceimg = Program.readBytes((uint)(p + faceimg_offset), 65580);
|
||||
var TelemetryPayload = Telemetry2Json(data);
|
||||
// Console.WriteLine(TelemetryPayload);
|
||||
|
||||
SendTelemetryHttp(Program.config["TelemetryUrl"], "0162b", TelemetryPayload, faceimg).GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
1
VERSION.txt
Normal file
1
VERSION.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
3.2.0
|
||||
24
assets/Info.plist
Normal file
24
assets/Info.plist
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>SplatTool</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>icon.icns</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>cc.crafterpika.splattool</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>SplatTool</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>3.2.0</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>1</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>12.7</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
assets/SplatTool@1024x1024-rounded.png
Normal file
BIN
assets/SplatTool@1024x1024-rounded.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 708 KiB |
BIN
assets/SplatTool@1024x1024.png
Normal file
BIN
assets/SplatTool@1024x1024.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 715 KiB |
3
assets/config.json
Normal file
3
assets/config.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"TelemetryUrl": "https://splatpost.spbr.net/post"
|
||||
}
|
||||
BIN
assets/demo.png
Normal file
BIN
assets/demo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2 MiB |
17
assets/entitlements.plist
Normal file
17
assets/entitlements.plist
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!-- MacOS will whine about the DLLs -->
|
||||
<key>com.apple.security.cs.disable-library-validation</key>
|
||||
<true/>
|
||||
|
||||
<!-- allow task_for_pid() to function -->
|
||||
<key>com.apple.security.cs.debugger</key>
|
||||
<true/>
|
||||
|
||||
<!-- C# Needs JIT for CIL -->
|
||||
<key>com.apple.security.cs.allow-jit</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
assets/icon.icns
Normal file
BIN
assets/icon.icns
Normal file
Binary file not shown.
7
assets/root_helper/build.sh
Executable file
7
assets/root_helper/build.sh
Executable file
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/zsh
|
||||
clang -target x86_64-apple-macos12.7 -o root_helper_x86 root_helper.c
|
||||
clang -target arm64-apple-macos12.7 -o root_helper_arm64 root_helper.c
|
||||
|
||||
lipo -create root_helper_x86 root_helper_arm64 -output root_helper
|
||||
rm -rf root_helper_x86
|
||||
rm -rf root_helper_arm64
|
||||
BIN
assets/root_helper/root_helper
Executable file
BIN
assets/root_helper/root_helper
Executable file
Binary file not shown.
36
assets/root_helper/root_helper.c
Normal file
36
assets/root_helper/root_helper.c
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <string.h>
|
||||
#include <limits.h>
|
||||
#include <libgen.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
char path[PATH_MAX];
|
||||
char dir[PATH_MAX];
|
||||
char script_path[PATH_MAX];
|
||||
char target_app[PATH_MAX];
|
||||
char command[PATH_MAX * 3];
|
||||
|
||||
realpath(argv[0], path);
|
||||
char *parent_dir = dirname(path);
|
||||
strncpy(dir, parent_dir, sizeof(dir) - 1);
|
||||
|
||||
if (geteuid() != 0) {
|
||||
snprintf(command, sizeof(command), "osascript -e \"do shell script \\\"%s\\\" with prompt \\\"SplatTool needs root for reading Cemu's memory.\\\" with administrator privileges\"", path);
|
||||
|
||||
int status = system(command);
|
||||
return WEXITSTATUS(status);
|
||||
}
|
||||
|
||||
snprintf(target_app, sizeof(target_app), "%s/SplatTool", dir);
|
||||
chdir(dir);
|
||||
|
||||
extern char **environ;
|
||||
char *new_argv[] = { target_app, NULL };
|
||||
|
||||
execve(target_app, new_argv, environ);
|
||||
|
||||
perror("execve failed");
|
||||
return 1;
|
||||
}
|
||||
46
build-osx-app.sh
Executable file
46
build-osx-app.sh
Executable file
|
|
@ -0,0 +1,46 @@
|
|||
#!/bin/zsh
|
||||
rm -rf dist
|
||||
rm -rf bin
|
||||
rm -rf obj
|
||||
rm -rf *.dmg
|
||||
|
||||
dotnet publish -r osx-x64 --self-contained true -p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true
|
||||
dotnet publish -r osx-arm64 --self-contained true -p:PublishSingleFile=true -p:EnableCompressionInSingleFile=true
|
||||
|
||||
INTEL_PUBLISH="bin/Release/net8.0/osx-x64/publish"
|
||||
ARM64_PUBLISH="bin/Release/net8.0/osx-arm64/publish"
|
||||
APP_ROOT="dist/SplatTool.app"
|
||||
|
||||
mkdir -p "$APP_ROOT/Contents/MacOS"
|
||||
mkdir -p "$APP_ROOT/Contents/Resources"
|
||||
mkdir -p "$APP_ROOT/Contents/Frameworks"
|
||||
|
||||
SHORT_SHA="$(git rev-parse --short HEAD)"
|
||||
VERSION=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" assets/Info.plist)
|
||||
|
||||
cp assets/Info.plist "$APP_ROOT/Contents/"
|
||||
cp assets/icon.icns "$APP_ROOT/Contents/Resources/"
|
||||
cp assets/config.json "$APP_ROOT/Contents/Resources"
|
||||
cp -r modmenu "$APP_ROOT/Contents/Resources/"
|
||||
cp icon.ico "$APP_ROOT/Contents/Resources/"
|
||||
cp assets/root_helper/root_helper "$APP_ROOT/Contents/MacOS"
|
||||
cp "$ARM64_PUBLISH/Photino.Native.dylib" "$APP_ROOT/Contents/Frameworks"
|
||||
lipo -create -output "$APP_ROOT/Contents/MacOS/SplatTool" "$INTEL_PUBLISH/SplatTool" "$ARM64_PUBLISH/SplatTool"
|
||||
lipo -create -output "$APP_ROOT/Contents/Frameworks/libnfd.dylib" "$INTEL_PUBLISH/libnfd.dylib" "$ARM64_PUBLISH/libnfd.dylib"
|
||||
|
||||
install_name_tool -add_rpath "@loader_path/../Frameworks" "$APP_ROOT/Contents/MacOS/SplatTool"
|
||||
install_name_tool -id "@rpath/libnfd.dylib" "$APP_ROOT/Contents/Frameworks/libnfd.dylib"
|
||||
install_name_tool -id "@rpath/Photino.Native.dylib" "$APP_ROOT/Contents/Frameworks/Photino.Native.dylib"
|
||||
|
||||
xattr -cr "$APP_ROOT"
|
||||
|
||||
chmod +x "$APP_ROOT/Contents/MacOS/SplatTool"
|
||||
chmod +x "$APP_ROOT/Contents/MacOS/root_helper"
|
||||
|
||||
codesign --remove-signature "$APP_ROOT"
|
||||
codesign --sign "-" --entitlements assets/entitlements.plist --force --deep --preserve-metadata=entitlements,requirements,flags,runtime --timestamp "$APP_ROOT"
|
||||
codesign -d --entitlements - "$APP_ROOT/Contents/MacOS/SplatTool"
|
||||
codesign -vvv --deep --strict "$APP_ROOT"
|
||||
|
||||
ln -sf "/Applications" "dist/Applications"
|
||||
hdiutil create -format UDZO -fs HFS+ -ov -volname "SplatTool" -srcfolder dist/ SplatTool-$VERSION-$SHORT_SHA-Universal-OSX.dmg
|
||||
150
httpMenu.cs
Normal file
150
httpMenu.cs
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
// modules
|
||||
using ModMenu;
|
||||
using Func;
|
||||
using System.IO;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using FreezeThread;
|
||||
using TelemetryWebpost;
|
||||
using SixLabors.ImageSharp.Processing.Processors.Normalization;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
namespace ModMenu {
|
||||
public class ModMenuRequest {
|
||||
public string? action { get; set; }
|
||||
public string[]? settings { get; set; }
|
||||
}
|
||||
|
||||
public class httpMenu {
|
||||
public static uint sessionId;
|
||||
public static List<GearQueueSlot> GearQueue = new List<GearQueueSlot>();
|
||||
public static uint Gender;
|
||||
public static uint Level;
|
||||
public static uint Cash;
|
||||
public static uint SeaSnails;
|
||||
public static SessionData session = new SessionData();
|
||||
static WorkThread sessiondIdThread = new WorkThread(() => {
|
||||
Codes.forceSessiondId(sessionId);
|
||||
});
|
||||
static WorkThread WriteGear = new WorkThread(() => {
|
||||
SaveEditor.WriteGearQueue();
|
||||
});
|
||||
static WorkThread ChangeGender = new WorkThread(() => {
|
||||
SaveEditor.ChangeGender(Gender);
|
||||
});
|
||||
static WorkThread ChangeLevel = new WorkThread(() => {
|
||||
SaveEditor.ChangeLevel(Level);
|
||||
});
|
||||
static WorkThread ChangeCash = new WorkThread(() => {
|
||||
SaveEditor.ChangeCash(Cash);
|
||||
});
|
||||
static WorkThread ChangeSeaSnails = new WorkThread(() => {
|
||||
SaveEditor.ChangeSeaSnails(SeaSnails);
|
||||
});
|
||||
static WorkThread TelemetryThread = new WorkThread(() => {
|
||||
Thread.Sleep(31000);
|
||||
SplatTelemetry.SendTelemetry();
|
||||
});
|
||||
public static string RunCode(ModMenuRequest data) {
|
||||
if (data?.action == null) return "0";
|
||||
|
||||
switch (data.action) {
|
||||
case "cbase":
|
||||
return $"Cemu Base: 0x{Program.cbase:X} (0x{Program.cbaseCorrected:X})<br><br>Total Wins: {Codes.TotalWins()}<br>Total Losses: {Codes.TotalLoss()}<br><br>Thread Status:<br>- Write Gear: {WriteGear.Status.ToString()}<br>- Telemetry: {TelemetryThread.Status.ToString()}<br>- Change Gender: {ChangeGender.Status.ToString()}<br>- Change Level: {ChangeLevel.Status.ToString()}<br>- Change Cash: {ChangeCash.Status.ToString()}<br>- Change Sea Snails: {ChangeSeaSnails.Status.ToString()}";
|
||||
|
||||
case "sessiondata":
|
||||
return Codes.GetSessionData();
|
||||
|
||||
case "namechange":
|
||||
if (data.settings?.Length > 0) Codes.NameChanger(data.settings[0]);
|
||||
return "1";
|
||||
|
||||
case "sessionid":
|
||||
if (data.settings?.Length > 0) {
|
||||
sessionId = (uint)Int32.Parse(data.settings[0]);
|
||||
HandleThread(sessiondIdThread);
|
||||
}
|
||||
return "1";
|
||||
|
||||
case "faceimgtga":
|
||||
Codes.faceimgtga(Int32.Parse(data.settings[0]));
|
||||
return "1";
|
||||
|
||||
case "OSType":
|
||||
return Program.OsType.ToString();
|
||||
|
||||
case "FetchGear":
|
||||
return SaveEditor.FetchAllGear();
|
||||
|
||||
case "WriteToGearQueue":
|
||||
SaveEditor.QueueGearSlot(UInt32.Parse(data.settings[0]), data.settings[1], UInt32.Parse(data.settings[2]), UInt32.Parse(data.settings[3]), UInt32.Parse(data.settings[4]), UInt32.Parse(data.settings[5]));
|
||||
return "1";
|
||||
|
||||
case "WriteGearQueue":
|
||||
if (WriteGear.Status == WorkThread.ThreadStatus.Stopped) {
|
||||
WriteGear.Start();
|
||||
} else if (WriteGear.Status == WorkThread.ThreadStatus.Running) {
|
||||
WriteGear.Stop();
|
||||
GearQueue = new List<GearQueueSlot>();
|
||||
GearQueue.Clear();
|
||||
}
|
||||
return "1";
|
||||
|
||||
case "ChangeGender":
|
||||
Gender = UInt32.Parse(data.settings[0]);
|
||||
HandleThread(ChangeGender);
|
||||
return "1";
|
||||
|
||||
case "ChangeLevel":
|
||||
try {
|
||||
Level = UInt32.Parse(data.settings[0]);
|
||||
} catch {
|
||||
Level = 0;
|
||||
}
|
||||
HandleThread(ChangeLevel);
|
||||
return "1";
|
||||
|
||||
case "ChangeCash":
|
||||
try {
|
||||
Cash = UInt32.Parse(data.settings[0]);
|
||||
} catch {
|
||||
Cash = 0;
|
||||
}
|
||||
HandleThread(ChangeCash);
|
||||
return "1";
|
||||
|
||||
case "ChangeSeaSnails":
|
||||
try {
|
||||
SeaSnails = UInt32.Parse(data.settings[0]);
|
||||
} catch {
|
||||
SeaSnails = 0;
|
||||
}
|
||||
HandleThread(ChangeSeaSnails);
|
||||
return "1";
|
||||
|
||||
case "DisconnectLobby":
|
||||
Codes.DisconnectFromLobby();
|
||||
return "1";
|
||||
|
||||
case "Webpost":
|
||||
HandleThread(TelemetryThread);
|
||||
return "1";
|
||||
|
||||
default:
|
||||
return "Not Found";
|
||||
}
|
||||
}
|
||||
|
||||
private static void HandleThread(WorkThread thread) {
|
||||
if (thread.Status == WorkThread.ThreadStatus.Stopped) {
|
||||
thread.Start();
|
||||
} else if (thread.Status == WorkThread.ThreadStatus.Running) {
|
||||
thread.Pause();
|
||||
} else if (thread.Status == WorkThread.ThreadStatus.Paused) {
|
||||
thread.Resume();
|
||||
} else {
|
||||
thread.Pause();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
icon.ico
Normal file
BIN
icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 227 KiB |
359
modmenu/ids.json
Normal file
359
modmenu/ids.json
Normal file
|
|
@ -0,0 +1,359 @@
|
|||
{
|
||||
"headgear": {
|
||||
"0": "ARINOMAMA (ID=0)",
|
||||
"1": "White Headband (ID=1)",
|
||||
"1000": "Urchins Cap (ID=1000)",
|
||||
"1001": "Lightweight Cap (ID=1001)",
|
||||
"1002": "Takoroka Mesh (ID=1002)",
|
||||
"1003": "Fashion Cap (ID=1003)",
|
||||
"1004": "Squid-Stitch Cap (ID=1004)",
|
||||
"1005": "Squidvader Cap (ID=1005)",
|
||||
"1006": "Camo Mesh Cap (ID=1006)",
|
||||
"1007": "5-Panel Cap (ID=1007)",
|
||||
"1008": "Zekko Mesh (ID=1008)",
|
||||
"1009": "Backwards Cap (ID=1009)",
|
||||
"1010": "2-Stripe Mesh Cap (ID=1010)",
|
||||
"1011": "Jet Cap (ID=1011)",
|
||||
"1012": "Cycling Cap (ID=1012)",
|
||||
"1013": "SQUID GIRL Hat (ID=1013)",
|
||||
"1014": "Cycle King Cap (ID=1014)",
|
||||
"1015": "Legendary Cap (ID=1015)",
|
||||
"1016": "CoroCoro Cap (ID=1016)",
|
||||
"2000": "Bobble Hat (ID=2000)",
|
||||
"2001": "Short Beanie (ID=2001)",
|
||||
"2002": "Striped Beanie (ID=2002)",
|
||||
"2003": "Sporty Bobble Hat (ID=2003)",
|
||||
"2004": "Special Forces Beret (ID=2004)",
|
||||
"2005": "Squid Nordic (ID=2005)",
|
||||
"3000": "Retro Specs (ID=3000)",
|
||||
"3001": "Splash Googles (ID=3001)",
|
||||
"3002": "Pilot Goggles (ID=3002)",
|
||||
"3003": "Colored Shadows (ID=3003)",
|
||||
"3004": "Black Arrowbands (ID=3004)",
|
||||
"3005": "Snorkel (ID=3005)",
|
||||
"3006": "White Arrowbands (ID=3006)",
|
||||
"3007": "Fake Contacts (ID=3007)",
|
||||
"3008": "18K Aviators (ID=3008)",
|
||||
"3009": "Full Moon Glasses (ID=3009)",
|
||||
"3010": "Octoglasses (ID=3010)",
|
||||
"4000": "Jungle Hat (ID=4000)",
|
||||
"4001": "Safari Hat (ID=4001)",
|
||||
"4002": "Camping Hat (ID=4002)",
|
||||
"4003": "Fugu Bell Hat (ID=4003)",
|
||||
"4004": "Bambo Hat (ID=4004)",
|
||||
"4005": "Straw Boater (ID=4005)",
|
||||
"4006": "Treasure Hunter (ID=4006)",
|
||||
"5000": "Studio Headphones (ID=5000)",
|
||||
"5001": "Colorful Headphones (ID=5001)",
|
||||
"5002": "Noise Cancellers (ID=5002)",
|
||||
"6000": "Golf Visor (ID=6000)",
|
||||
"6001": "FishFry Visor (ID=6001)",
|
||||
"6002": "Sun Visor (ID=6002)",
|
||||
"7000": "Cycle Helmet (ID=7000)",
|
||||
"7002": "Stealth Goggles (ID=7002)",
|
||||
"7003": "Tentacles Helmet (ID=7003)",
|
||||
"7004": "Skate Helmet (ID=7004)",
|
||||
"7005": "Visor Skate Helmet (ID=7005)",
|
||||
"8000": "Gas Mask (ID=8000)",
|
||||
"8001": "Paintball Mask (ID=8001)",
|
||||
"8002": "Paisley Bandana (ID=8002)",
|
||||
"8003": "Skull Bandana (ID=8003)",
|
||||
"9001": "B-Ball Headband (ID=9001)",
|
||||
"9002": "Squash Headband (ID=9002)",
|
||||
"9003": "Tennis Headband (ID=9003)",
|
||||
"9004": "Joggling Headband (ID=9004)",
|
||||
"9005": "Soccer Headband (ID=9005)",
|
||||
"9006": "Traditional Headband (ID=9006)",
|
||||
"25000": "Squid Hairclip (ID=25000)",
|
||||
"25001": "Samurai Helmet (ID=25001)",
|
||||
"25002": "Power Mask (ID=25002)",
|
||||
"27000": "Hero Headset Replica (ID=27000)",
|
||||
"27001": "?MSN001 (ID=27001)",
|
||||
"27002": "?MSN002 (ID=27002)",
|
||||
"27003": "?MSN003 (ID=27003)",
|
||||
"27004": "?MSN004 (ID=27004)",
|
||||
"28000": "Octoling Scope (ID=28000)",
|
||||
"28001": "ヘルメットライバル用強 (ID=28001)",
|
||||
"29003": "?SubInk_Save (ID=29003)",
|
||||
"29004": "?InkRecovery_Up (ID=29004)",
|
||||
"29005": "?HumanMove_Up (ID=29005)",
|
||||
"29006": "?SquidMove_Up (ID=29006)",
|
||||
"29007": "?SpecialIncrease_Up (ID=29007)",
|
||||
"29008": "?SpecialTime_Up (ID=29008)",
|
||||
"29009": "?RespawnTime_Save (ID=29009)",
|
||||
"29010": "?RespawnSpecialGauge_Save (ID=29010)",
|
||||
"29011": "?JumpTime_Save (ID=29011)",
|
||||
"29012": "?BombDistance_Up (ID=29012)",
|
||||
"29013": "?StartAllUp (ID=29013)",
|
||||
"29014": "?EndAllUp (ID=29014)",
|
||||
"29015": "?MinorityUp (ID=29015)",
|
||||
"29016": "?ComeBack (ID=29016)",
|
||||
"29500": "?SUP000 (ID=29500)",
|
||||
"29501": "?SUP001 (ID=29501)"
|
||||
},
|
||||
"clothes": {
|
||||
"0": "?NoClothes (ID=0)",
|
||||
"1": "Basic Tee (ID=1)",
|
||||
"1000": "White Tee (ID=1000)",
|
||||
"1001": "Black Squideye (ID=1001)",
|
||||
"1003": "Sky Blue Squideye (ID=1003)",
|
||||
"1004": "RockenBerg White (ID=1004)",
|
||||
"1005": "RockenBerg Black (ID=1005)",
|
||||
"1006": "Black Tee (ID=1006)",
|
||||
"1007": "Sunny Day Tee (ID=1007)",
|
||||
"1008": "Rainy Day Tee (ID=1008)",
|
||||
"1009": "Reggae Tee (ID=1009)",
|
||||
"1010": "Fugu Tee (ID=1010)",
|
||||
"1011": "Mint Tee (ID=1011)",
|
||||
"1012": "Grape Tee (ID=1012)",
|
||||
"1013": "Red Vector Tee (ID=1013)",
|
||||
"1014": "Grey Vector Tee (ID=1014)",
|
||||
"1015": "Blue Peaks Tee (ID=1015)",
|
||||
"1016": "Ivory Peaks Tee (ID=1016)",
|
||||
"1017": "Squid-Stitch Tee (ID=1017)",
|
||||
"1018": "Pirate Stripes Tee (ID=1018)",
|
||||
"1019": "Sailor Stripes Tee (ID=1019)",
|
||||
"1020": "White 8-Bit FishFry (ID=1020)",
|
||||
"1021": "Black 8-Bit FishFry (ID=1021)",
|
||||
"1022": "White Anchor Tee (ID=1022)",
|
||||
"1023": "Black Anchor Tee (ID=1023)",
|
||||
"1024": "White Line Tee (ID=1024)",
|
||||
"1025": "Black Pipe Tee (ID=1025)",
|
||||
"1026": "Carnivore Tee (ID=1026)",
|
||||
"1027": "Pearl Tee (ID=1027)",
|
||||
"1028": "Octo Tee (ID=1028)",
|
||||
"1029": "Herbivore Tee (ID=1029)",
|
||||
"2000": "White Striped LS (ID=2000)",
|
||||
"2001": "Black LS (ID=2001)",
|
||||
"2002": "Purple Camo LS (ID=2002)",
|
||||
"2003": "Navy Striped LS (ID=2003)",
|
||||
"2004": "Zekko Baseball LS (ID=2004)",
|
||||
"2005": "Varsity Baseball (ID=2005)",
|
||||
"2006": "Black Baseball LS (ID=2006)",
|
||||
"2007": "White Baseball LS (ID=2007)",
|
||||
"2008": "White LS (ID=2008)",
|
||||
"2009": "Green Striped LS (ID=2009)",
|
||||
"2010": "Squidmark LS (ID=2010)",
|
||||
"2011": "Zink LS (ID=2011)",
|
||||
"2012": "Striped Peaks LS (ID=2012)",
|
||||
"3000": "White Layered LS (ID=3000)",
|
||||
"3001": "Yellow Layered LS (ID=3001)",
|
||||
"3002": "Layered Camo LS (ID=3002)",
|
||||
"3003": "Black Layered LS (ID=3003)",
|
||||
"3004": "Zink Layered LS (ID=3004)",
|
||||
"3005": "Layered Anchor LS (ID=3005)",
|
||||
"3006": "Choco Layered LS (ID=3006)",
|
||||
"3007": "Part-Time Pirate (ID=3007)",
|
||||
"3008": "Layered Vector LS (ID=3008)",
|
||||
"3009": "Green Tee (ID=3009)",
|
||||
"4000": "Pink Shrimp Polo (ID=4000)",
|
||||
"4001": "Striped Rugby (ID=4001)",
|
||||
"4002": "Tricolor Rugby (ID=4002)",
|
||||
"4003": "Sage Green Polo (ID=4003)",
|
||||
"4004": "Black Polo (ID=4004)",
|
||||
"4005": "Cycling Shirt (ID=4005)",
|
||||
"4006": "Cycle King Jersey (ID=4006)",
|
||||
"4007": "Slipstream United (ID=4007)",
|
||||
"4008": "FC Albacore (ID=4008)",
|
||||
"5000": "Olive Ski Jacket (ID=5000)",
|
||||
"5002": "Berry Ski Jacket (ID=5002)",
|
||||
"5003": "Varsity Jacket (ID=5003)",
|
||||
"5004": "School Jersey (ID=5004)",
|
||||
"5005": "Green Cardigan (ID=5005)",
|
||||
"5006": "Black Inky Rider (ID=5006)",
|
||||
"5007": "White Inky Rider (ID=5007)",
|
||||
"5008": "Retro Gamer Jersey (ID=5008)",
|
||||
"5009": "Orange Cardigan (ID=5009)",
|
||||
"5010": "Forge Inkling Perka (ID=5010)",
|
||||
"5011": "Forge Octarian Jacket (ID=5011)",
|
||||
"5012": "Blue Sailor Suit (ID=5012)",
|
||||
"5013": "White Sailor Suit (ID=5013)",
|
||||
"5014": "Squid Satin Jacket (ID=5014)",
|
||||
"5015": "Zapfish Satin Jacket (ID=5015)",
|
||||
"5016": "Krak-On 528 (ID=5016)",
|
||||
"6000": "B-Ball Vest (Home) (ID=6000)",
|
||||
"6001": "B-Ball Vest (Away) (ID=6001)",
|
||||
"6002": "SQUID GIRL Tunic (ID=6002)",
|
||||
"7000": "Grey College Sweat (ID=7000)",
|
||||
"7001": "Squidmark Sweat (ID=7001)",
|
||||
"7002": "Retro Sweat (ID=7002)",
|
||||
"7003": "Firefin Sweat Navy (ID=7003)",
|
||||
"7004": "Navy Collage Sweat (ID=7004)",
|
||||
"7005": "Reel Sweat (ID=7005)",
|
||||
"7006": "Anchor Sweat (ID=7006)",
|
||||
"8000": "Lumberjack Shirt (ID=8000)",
|
||||
"8001": "Rodeo Shirt (ID=8001)",
|
||||
"8002": "Green Check Shirt (ID=8002)",
|
||||
"8003": "White Shirt (ID=8003)",
|
||||
"8004": "Urchins Jersey (ID=8004)",
|
||||
"8005": "Aloha Shirt (ID=8005)",
|
||||
"8006": "Red Check Shirt (ID=8006)",
|
||||
"8007": "Baby Jelly Shirt (ID=8007)",
|
||||
"8008": "Baseball Jersey (ID=8008)",
|
||||
"8009": "Grey Mixed Shirt (ID=8009)",
|
||||
"8010": "Vintage Check (ID=8010)",
|
||||
"8011": "Round Collar Shirt (ID=8011)",
|
||||
"8012": "Logo Aloha Shirt (ID=8012)",
|
||||
"8013": "Striped Shirt (ID=8013)",
|
||||
"8014": "Linen Shirt (ID=8014)",
|
||||
"8015": "Shirt and Tie (ID=8015)",
|
||||
"8016": "Traditional Apron (ID=8016)",
|
||||
"9000": "Mountain Gilet (ID=9000)",
|
||||
"9001": "Forest Gilet (ID=9001)",
|
||||
"9002": "Dark Urban Gilet (ID=9002)",
|
||||
"9003": "Yellow Urban Gilet (ID=9003)",
|
||||
"9004": "Squid Pattern Waistcoat (ID=9004)",
|
||||
"9005": "Squidstar Waistcoat (ID=9005)",
|
||||
"10000": "Camo Zip Hoodie (ID=10000)",
|
||||
"10001": "Green Zip Hoodie (ID=10001)",
|
||||
"10002": "Zekko Hoodie (ID=10002)",
|
||||
"10003": "CoroCoro Hoodie (ID=10003)",
|
||||
"25000": "School Uniform (ID=25000)",
|
||||
"25001": "Samurai Jacket (ID=25001)",
|
||||
"25002": "Power Armour (ID=25002)",
|
||||
"27000": "?MSN000 (ID=27000)",
|
||||
"27001": "?MSN001 (ID=27001)",
|
||||
"27002": "?MSN002 (ID=27002)",
|
||||
"27003": "?MSN003 (ID=27003)",
|
||||
"27004": "Armor Jacket Replica (ID=27004)",
|
||||
"28000": "Octoling Armor (ID=28000)",
|
||||
"26000": "Splatfest Tee (ID=26000)",
|
||||
"29000": "Attack_Up (ID=29000)",
|
||||
"29001": "Defense_Up (ID=29001)",
|
||||
"29002": "MainInk_Save (ID=29002)",
|
||||
"29003": "SubInk_Save (ID=29003)",
|
||||
"29004": "InkRecovery_Up (ID=29004)",
|
||||
"29005": "HumanMove_Up (ID=29005)",
|
||||
"29006": "SquidMove_Up (ID=29006)",
|
||||
"29007": "SpecialIncrease_Up (ID=29007)",
|
||||
"29008": "SpecialTime_Up (ID=29008)",
|
||||
"29009": "RespawnTime_Save (ID=29009)",
|
||||
"29010": "RespawnSpecialGauge_Save (ID=29010)",
|
||||
"29011": "JumpTime_Save (ID=29011)",
|
||||
"29012": "BombDistance_Up (ID=29012)",
|
||||
"29013": "MarkingCancel (ID=29013)",
|
||||
"29014": "SquidMoveSpatter_Reduction (ID=29014)",
|
||||
"29015": "DeathMarking (ID=29015)",
|
||||
"29016": "RespawnRadar (ID=29016)",
|
||||
"29500": "?SUP000 (ID=29500)",
|
||||
"29501": "?SUP001 (ID=29501)"
|
||||
},
|
||||
"shoes": {
|
||||
"0": "ありのまま (ID=0)",
|
||||
"1": "Cream Basics (ID=1)",
|
||||
"1000": "Blue Lo-Tops (ID=1000)",
|
||||
"1001": "Banana Basics (ID=1001)",
|
||||
"1002": "LE Lo-Tops (ID=1002)",
|
||||
"1003": "White Seahorses (ID=1003)",
|
||||
"1004": "Orange Lo-Tops (ID=1004)",
|
||||
"1005": "Black Seahorses (ID=1005)",
|
||||
"1006": "Clownfish Basics (ID=1006)",
|
||||
"1007": "Yellow Seahorses (ID=1007)",
|
||||
"1008": "Strapping Whites (ID=1008)",
|
||||
"1009": "Strapping Reds (ID=1009)",
|
||||
"1010": "Soccer Cleats (ID=1010)",
|
||||
"1011": "LE Soccer Cleats (ID=1011)",
|
||||
"2000": "Red Hi-Horses (ID=2000)",
|
||||
"2001": "Zombie Hi-Horses (ID=2001)",
|
||||
"2002": "Cream Hi-Tops (ID=2002)",
|
||||
"2003": "Purple Hi-Horses (ID=2003)",
|
||||
"2004": "Hunter Hi-Tops (ID=2004)",
|
||||
"2005": "Red Hi-Tops (ID=2005)",
|
||||
"2006": "Gold Hi_Horses (ID=2006)",
|
||||
"2007": "SQUID GIRL Shoes (ID=2007)",
|
||||
"2008": "Shark Mawcasins (ID=2008)",
|
||||
"2009": "Mawcasins (ID=2009)",
|
||||
"3000": "Pink Trainers (ID=3000)",
|
||||
"3001": "Orange Arrows (ID=3001)",
|
||||
"3002": "Neon Sea Slugs (ID=3002)",
|
||||
"3003": "White Arrows (ID=3003)",
|
||||
"3004": "Cyan Trainers (ID=3004)",
|
||||
"3005": "Purple Sea Slugs (ID=3005)",
|
||||
"3006": "Red Sea Slugs (ID=3006)",
|
||||
"3007": "Blue Sea Slugs (ID=3007)",
|
||||
"3008": "Crazy Arrows (ID=3008)",
|
||||
"3009": "Black Trainers (ID=3009)",
|
||||
"4000": "Oyster Clogs (ID=4000)",
|
||||
"4001": "Choco Clogs (ID=4001)",
|
||||
"4002": "Blueberry Casuals (ID=4002)",
|
||||
"4003": "Plum Casuals (ID=4003)",
|
||||
"4006": "Traditional Sandals (ID=4006)",
|
||||
"5000": "Trail Boots (ID=5000)",
|
||||
"5001": "Custom Trail Boots (ID=5001)",
|
||||
"5002": "Pro Trail Boots (ID=5002)",
|
||||
"6000": "Moto Boots (ID=6000)",
|
||||
"6001": "Tan Work Boots (ID=6001)",
|
||||
"6002": "Red Work Boots (ID=6002)",
|
||||
"6003": "Blue Moto Boots (ID=6003)",
|
||||
"6004": "Green Rain Boots (ID=6004)",
|
||||
"6005": "Acerola Rain Boots (ID=6005)",
|
||||
"6006": "Punk Whites (ID=6006)",
|
||||
"6007": "Punk Cherries (ID=6007)",
|
||||
"6008": "Punk Yellows (ID=6008)",
|
||||
"6009": "Bubble Rain Boots (ID=6009)",
|
||||
"6010": "Snowy Down Boots (ID=6010)",
|
||||
"6011": "Icy Down Boots (ID=6011)",
|
||||
"7000": "Blue Slip-Ons (ID=7000)",
|
||||
"7001": "Red Slip-Ons (ID=7001)",
|
||||
"7002": "Squid-Stitch Slip-Ons (ID=7002)",
|
||||
"8000": "White Kicks (ID=8000)",
|
||||
"8001": "Cherry Kicks (ID=8001)",
|
||||
"8002": "Turquoise Kicks (ID=8002)",
|
||||
"8003": "Squink Wingtips (ID=8003)",
|
||||
"8004": "Roasted Brogues (ID=8004)",
|
||||
"25000": "School Shoes (ID=25000)",
|
||||
"25001": "Samurai Shoes (ID=25001)",
|
||||
"25002": "Power Boots (ID=25002)",
|
||||
"27000": "Hero Runner Replica (ID=27000)",
|
||||
"27001": "?MSN001 (ID=27001)",
|
||||
"27002": "?MSN002 (ID=27002)",
|
||||
"27003": "?MSN003 (ID=27003)",
|
||||
"27004": "Armor Boots Replica (ID=27004)",
|
||||
"28000": "Octoling Boots (ID=28000)",
|
||||
"29000": "?Attack_Up (ID=29000)",
|
||||
"29001": "?Defense_Up (ID=29001)",
|
||||
"29002": "?MainInk_Save (ID=29002)",
|
||||
"29003": "?SubInk_Save (ID=29003)",
|
||||
"29004": "?Inkrecovery_Up (ID=29004)",
|
||||
"29005": "?HumanMove_Up (ID=29005)",
|
||||
"29006": "?SquidMove_Up (ID=29006)",
|
||||
"29007": "?SpecialIncrease_Up (ID=29007)",
|
||||
"29008": "?SpecialTime_Up (ID=29008)",
|
||||
"29009": "?RespawnTime_Save (ID=29009)",
|
||||
"29010": "?RespawnSpecialGauge_Save (ID=29010)",
|
||||
"29011": "?JumpTime_Save (ID=29011)",
|
||||
"29012": "?BombDistance_Up (ID=29012)",
|
||||
"29013": "?TrapDetect (ID=29013)",
|
||||
"29014": "?EnemyInkEffect_Reduction (ID=29014)",
|
||||
"29015": "?SuperJumpSign_Hide (ID=29015)",
|
||||
"29500": "?SUP000 (ID=29500)",
|
||||
"29501": "?SUP001 (ID=29501)"
|
||||
},
|
||||
"abilities": {
|
||||
"0": "Damage Up",
|
||||
"1": "Defense Up",
|
||||
"2": "Ink Saver (Main)",
|
||||
"3": "Ink Saver (Sub)",
|
||||
"4": "Ink Recovery Up",
|
||||
"5": "Run Speed Up",
|
||||
"6": "Swim Speed Up",
|
||||
"7": "Special Charge Up",
|
||||
"8": "Special Duration Up",
|
||||
"9": "Quick Respawn",
|
||||
"10": "Special Saver",
|
||||
"11": "Quick Super Jump",
|
||||
"12": "Bomb Range Up",
|
||||
"100": "Opening Gambit",
|
||||
"101": "Last Ditch Effort",
|
||||
"102": "Tenacity",
|
||||
"103": "Comeback",
|
||||
"104": "Cold Blooded",
|
||||
"105": "Ninja Squid",
|
||||
"106": "Haunt",
|
||||
"107": "Recon",
|
||||
"108": "Bomb Sniffer",
|
||||
"109": "Ink Resistance Up",
|
||||
"110": "Stealth Jump",
|
||||
"111": "S_Abillity_Locked"
|
||||
}
|
||||
}
|
||||
189
modmenu/index.html
Normal file
189
modmenu/index.html
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>SplatTool</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<h1 class="main-title">SplatTool Mod Menu</h1>
|
||||
|
||||
<div class="tabs-container">
|
||||
<div class="tab-menu">
|
||||
<button class="tab-link active" onclick="openTab(event, 'Tab1')">Player & Lobby</button>
|
||||
<button class="tab-link" onclick="openTab(event, 'Tab8')">Save Editor</button>
|
||||
<button class="tab-link" onclick="openTab(event, 'Tab4')">Utilities</button>
|
||||
<button class="tab-link" onclick="openTab(event, 'Tab5')">PID Grabber</button>
|
||||
<button class="tab-link" onclick="openTab(event, 'Tab6')">Stats</button>
|
||||
<button class="tab-link" onclick="openTab(event, 'Tab7')">About</button>
|
||||
</div>
|
||||
|
||||
<div id="Tab1" class="tab-content active">
|
||||
<div class="profile-card">
|
||||
<div class="input-row">
|
||||
<div class="input-group">
|
||||
<span class="input-label">Name Changer:</span>
|
||||
<input type="text" id="name-input" placeholder="Enter name..." autocomplete="off">
|
||||
<button class="action-btn" onclick="namechange()">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-row">
|
||||
<span class="input-label">Disconnect From Lobby:</span>
|
||||
<div class="input-group">
|
||||
<div class="spacer"></div> <button class="action-btn" onclick="execute('DisconnectLobby')">Apply</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="Tab8" class="tab-content">
|
||||
<div class="profile-card">
|
||||
<h3 style="margin-top: 0; font-size: 14px; color: var(--accent);">Gear Editor</h3>
|
||||
<div class="input-row">
|
||||
<span class="input-label">Category:</span>
|
||||
<select id="gear-category" class="dropdown" style="flex: 1;" onchange="onCategoryChange()">
|
||||
<option value="headgear">Headgear</option>
|
||||
<option value="clothing">Clothing</option>
|
||||
<option value="shoes">Shoes</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div id="gear-selection-grid" class="pid-list" style="margin-bottom: 15px; border: 1px solid var(--border); padding: 5px;">
|
||||
<p style="text-align: center; opacity: 0.5; width: 100%;">Select a category to load gear...</p>
|
||||
</div>
|
||||
|
||||
<hr style="border: 0; border-top: 1px solid var(--border); margin: 15px 0;">
|
||||
|
||||
<div id="editor-fields">
|
||||
<div class="input-row">
|
||||
<span class="input-label">Item to Write:</span>
|
||||
<select id="gear-item" class="dropdown" style="flex: 1;"></select>
|
||||
</div>
|
||||
<div class="input-row">
|
||||
<span class="input-label">Slot Index:</span>
|
||||
<input type="number" id="gear-slot-idx" value="0" readonly class="dropdown" style="width: 60px; opacity: 0.7;">
|
||||
</div>
|
||||
<div class="input-row">
|
||||
<span class="input-label">Sub 1:</span>
|
||||
<select id="ability-sub1" class="dropdown" style="flex: 1;"></select>
|
||||
</div>
|
||||
<div class="input-row">
|
||||
<span class="input-label">Sub 2:</span>
|
||||
<select id="ability-sub2" class="dropdown" style="flex: 1;"></select>
|
||||
</div>
|
||||
<div class="input-row">
|
||||
<span class="input-label">Sub 3:</span>
|
||||
<select id="ability-sub3" class="dropdown" style="flex: 1;"></select>
|
||||
</div>
|
||||
<div class="input-row">
|
||||
<button id="gear-thread-btn" class="action-btn" style="flex: 1; background: #333;" onclick="toggleGearThread()">Write Queue</button>
|
||||
<button class="action-btn" style="flex: 1;" onclick="applyGearChange()">Write to Queue</button>
|
||||
</div>
|
||||
</div>
|
||||
<br><br>
|
||||
<h3 style="margin-top: 0; font-size: 14px; color: var(--accent);">Player & Other</h3>
|
||||
<div class="input-row">
|
||||
<div class="label-stack">
|
||||
<span class="input-label">Change Gender:</span>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<select id="gender-select" class="dropdown" style="flex: 1;">
|
||||
<option value="0">Girl</option>
|
||||
<option value="1">Boy</option>
|
||||
<option value="2">Octoling (Rival)</option>
|
||||
</select>
|
||||
<button class="action-btn" onclick="GenderChange()">Toggle Thread</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-row">
|
||||
<div class="input-group">
|
||||
<span class="input-label">Change Level:</span>
|
||||
<input type="text" id="level-input" placeholder="50..." autocomplete="off">
|
||||
<button class="action-btn" onclick="Level()">Toggle Thread</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-row">
|
||||
<div class="input-group">
|
||||
<span class="input-label">Change Cash:</span>
|
||||
<input type="text" id="cash-input" placeholder="9999..." autocomplete="off">
|
||||
<button class="action-btn" onclick="Cash()">Toggle Thread</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-row">
|
||||
<div class="input-group">
|
||||
<span class="input-label">Change Sea Snails:</span>
|
||||
<input type="text" id="snails-input" placeholder="999..." autocomplete="off">
|
||||
<button class="action-btn" onclick="SeaSnails()">Toggle Thread</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="Tab4" class="tab-content">
|
||||
<div class="profile-card">
|
||||
<div class="input-row">
|
||||
<div class="input-group">
|
||||
<span class="input-label">Force Session ID:</span>
|
||||
<input type="text" id="session-input" placeholder="Enter Session ID..." autocomplete="off">
|
||||
<button class="action-btn" onclick="sessionid()">Toggle Thread</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-row">
|
||||
<div class="label-stack">
|
||||
<span class="input-label">Dump faceimg.tga:</span>
|
||||
<span class="input-note">Disable Anti-Telemetry.</span>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<select id="format-select" class="dropdown" style="flex: 1;">
|
||||
<option value="0">TGA</option>
|
||||
<option value="1">PNG</option>
|
||||
</select>
|
||||
<button class="action-btn" onclick="faceimgtga();alert('faceimg.tga should be dumped.')">Dump</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="Tab5" class="tab-content">
|
||||
<div class="profile-card">
|
||||
<div class="input-row">
|
||||
<span class="input-label">Session Control:</span>
|
||||
<button class="action-btn" style="flex: 1;" onclick="fetchPids()">Fetch Current PIDs</button>
|
||||
</div>
|
||||
<div id="pid-display" class="pid-list">
|
||||
<p style="text-align: center; opacity: 0.5; margin-top: 20px;">No data fetched yet.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="Tab6" class="tab-content">
|
||||
<div class="profile-card">
|
||||
<h3 style="margin-top: 0;">Stats:</h3>
|
||||
<div id="cbase">Waiting for fetch...</div>
|
||||
<br><br>
|
||||
<div class="input-row">
|
||||
<div class="label-stack">
|
||||
<span class="input-label">Webpost:</span>
|
||||
<span class="input-note">Only works with SPFN.</span>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<div class="spacer"></div> <button class="action-btn" onclick="execute('Webpost')">Toggle Thread</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="Tab7" class="tab-content">
|
||||
<div class="profile-card">
|
||||
<h3 style="margin-top: 0;">SplatTool v3.2.0</h3>
|
||||
<div>Written by CrafterPika</div>
|
||||
<div>Special Thanks: Winterberry, Javi.ig, Tombuntu, KittenTM, apoplexy</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
210
modmenu/script.js
Normal file
210
modmenu/script.js
Normal file
|
|
@ -0,0 +1,210 @@
|
|||
let gearData = null;
|
||||
let gearThreadActive = false;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadGearData();
|
||||
sendToCSharp("OSType");
|
||||
});
|
||||
|
||||
async function loadGearData() {
|
||||
try {
|
||||
const response = await fetch('ids.json');
|
||||
gearData = await response.json();
|
||||
populateAbilities();
|
||||
syncGearDropdown();
|
||||
} catch (e) {
|
||||
console.error("Gear IDs failed to load. Check if ids.json exists.", e);
|
||||
}
|
||||
}
|
||||
|
||||
function onCategoryChange() {
|
||||
syncGearDropdown();
|
||||
const grid = document.getElementById('gear-selection-grid');
|
||||
grid.innerHTML = '<p style="text-align: center; width: 100%; padding: 20px;">Fetching Gear...</p>';
|
||||
execute('FetchGear');
|
||||
}
|
||||
|
||||
function syncGearDropdown() {
|
||||
if (!gearData) return;
|
||||
const cat = document.getElementById('gear-category').value;
|
||||
|
||||
const key = (cat === "clothing") ? "clothes" : cat;
|
||||
const select = document.getElementById('gear-item');
|
||||
|
||||
if (!select) return;
|
||||
select.innerHTML = '';
|
||||
|
||||
if (gearData[key]) {
|
||||
for (const [id, name] of Object.entries(gearData[key])) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = id;
|
||||
opt.textContent = name;
|
||||
select.appendChild(opt);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function populateAbilities() {
|
||||
const selectors = ['ability-sub1', 'ability-sub2', 'ability-sub3'];
|
||||
selectors.forEach(id => {
|
||||
const select = document.getElementById(id);
|
||||
if (!select || !gearData.abilities) return;
|
||||
select.innerHTML = '';
|
||||
for (const [val, name] of Object.entries(gearData.abilities)) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = val;
|
||||
opt.textContent = name;
|
||||
select.appendChild(opt);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
window.external.receiveMessage(message => {
|
||||
// console.log(message)
|
||||
if (message.startsWith("Cemu Base:")) {
|
||||
document.getElementById("cbase").innerHTML = message;
|
||||
}
|
||||
|
||||
if (message === "2") {
|
||||
alert("MacOS Build is Untested. Expect Issues!");
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(message);
|
||||
//console.log("Parsed Data:", data);
|
||||
const container = document.getElementById("pid-display");
|
||||
|
||||
if (data.Players) {
|
||||
container.innerHTML = `<p style="text-align: center; opacity: 0.5; margin-top: 20px;">Fetching data....</p>`;
|
||||
let html = `<div style="grid-column: span 2; color: var(--highlight); font-family: monospace; font-size: 14px; margin-bottom: 5px; border-bottom: 1px solid #222; padding-bottom: 5px;">SESSION: ${data.SessionID}</div>`;
|
||||
data.Players.forEach((p, i) => {
|
||||
html += `<div class="pid-card"><b>P${i}: ${p.Name}</b><br><small>PID: ${p.PID}</small><br><small>NNID: ${p.NNID}</small></div>`;
|
||||
});
|
||||
container.innerHTML = html || '<p style="text-align: center;">No players found.</p>';
|
||||
}
|
||||
|
||||
else if (data.headgear || data.clothing || data.shoes || data.clothes) {
|
||||
renderGearGrid(data);
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e.message);
|
||||
}
|
||||
});
|
||||
|
||||
function renderGearGrid(fullData) {
|
||||
const category = document.getElementById('gear-category').value;
|
||||
const grid = document.getElementById('gear-selection-grid');
|
||||
grid.innerHTML = "";
|
||||
|
||||
let items = fullData[category];
|
||||
if (!items && category === "clothing") items = fullData["clothes"];
|
||||
|
||||
if (!items || items.length === 0) {
|
||||
grid.innerHTML = '<p style="text-align: center; width: 100%; padding: 20px;">No gear found in this category.</p>';
|
||||
return;
|
||||
}
|
||||
|
||||
const idLookupKey = (category === "clothing") ? "clothes" : category;
|
||||
|
||||
items.forEach((item, index) => {
|
||||
const isEmpty = (item.GearID === 4294967295 || item.GearID === -1);
|
||||
|
||||
let gearName = "Unknown Item";
|
||||
if (isEmpty) {
|
||||
gearName = "Empty Slot (+)";
|
||||
} else if (gearData && gearData[idLookupKey] && gearData[idLookupKey][item.GearID]) {
|
||||
gearName = gearData[idLookupKey][item.GearID];
|
||||
} else {
|
||||
gearName = `ID: ${item.GearID}`;
|
||||
}
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'pid-card';
|
||||
card.style.cursor = 'pointer';
|
||||
|
||||
if (isEmpty) {
|
||||
card.style.borderLeft = '4px dashed #444';
|
||||
card.style.opacity = '0.7';
|
||||
}
|
||||
|
||||
card.innerHTML = `<b>${gearName}</b><br><small>Slot: ${index}</small>`;
|
||||
|
||||
card.onclick = () => {
|
||||
document.querySelectorAll('.pid-card').forEach(c => {
|
||||
c.style.borderColor = 'var(--border)';
|
||||
c.style.background = 'transparent';
|
||||
});
|
||||
card.style.borderColor = 'var(--highlight)';
|
||||
card.style.background = 'rgba(3, 218, 198, 0.05)';
|
||||
|
||||
syncGearDropdown();
|
||||
document.getElementById('gear-slot-idx').value = index;
|
||||
|
||||
if (!isEmpty) {
|
||||
document.getElementById('gear-item').value = item.GearID;
|
||||
|
||||
document.getElementById('ability-sub1').value = (item.Slot1 === 4294967295) ? 0 : item.Slot1;
|
||||
document.getElementById('ability-sub2').value = (item.Slot2 === 4294967295) ? 0 : item.Slot2;
|
||||
document.getElementById('ability-sub3').value = (item.Slot3 === 4294967295) ? 0 : item.Slot3;
|
||||
} else {
|
||||
document.getElementById('gear-item').selectedIndex = 0;
|
||||
document.getElementById('ability-sub1').value = 0;
|
||||
document.getElementById('ability-sub2').value = 0;
|
||||
document.getElementById('ability-sub3').value = 0;
|
||||
}
|
||||
};
|
||||
|
||||
grid.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
function applyGearChange() {
|
||||
const category = document.getElementById('gear-category').value;
|
||||
const slot = document.getElementById('gear-slot-idx').value;
|
||||
const id = document.getElementById('gear-item').value;
|
||||
const s1 = document.getElementById('ability-sub1').value;
|
||||
const s2 = document.getElementById('ability-sub2').value;
|
||||
const s3 = document.getElementById('ability-sub3').value;
|
||||
|
||||
sendToCSharp("WriteToGearQueue", [slot, category, id, s1, s2, s3]);
|
||||
}
|
||||
|
||||
function toggleGearThread() {
|
||||
gearThreadActive = !gearThreadActive;
|
||||
const btn = document.getElementById('gear-thread-btn');
|
||||
btn.textContent = gearThreadActive ? "Stop Queue" : "Write Queue";
|
||||
btn.style.background = gearThreadActive ? "var(--highlight)" : "#333";
|
||||
sendToCSharp("WriteGearQueue");
|
||||
}
|
||||
|
||||
function sendToCSharp(action, settings = []) {
|
||||
if (window.external && window.external.sendMessage) {
|
||||
window.external.sendMessage(JSON.stringify({ action: action, settings: settings }));
|
||||
}
|
||||
}
|
||||
|
||||
function openTab(evt, tabName) {
|
||||
const content = document.getElementsByClassName("tab-content");
|
||||
for (let i = 0; i < content.length; i++) {
|
||||
content[i].classList.remove("active");
|
||||
content[i].style.display = "none";
|
||||
}
|
||||
const links = document.getElementsByClassName("tab-link");
|
||||
for (let i = 0; i < links.length; i++) links[i].classList.remove("active");
|
||||
|
||||
document.getElementById(tabName).style.display = "block";
|
||||
document.getElementById(tabName).classList.add("active");
|
||||
evt.currentTarget.classList.add("active");
|
||||
if (tabName === 'Tab6') fetchCemuBase();
|
||||
}
|
||||
|
||||
function namechange() { sendToCSharp("namechange", [document.getElementById("name-input").value]); }
|
||||
function sessionid() { sendToCSharp("sessionid", [document.getElementById("session-input").value]); }
|
||||
function faceimgtga() { sendToCSharp("faceimgtga", [document.getElementById("format-select").value]); }
|
||||
function GenderChange() { sendToCSharp("ChangeGender", [document.getElementById("gender-select").value]); }
|
||||
function Level() { sendToCSharp("ChangeLevel", [document.getElementById("level-input").value]); }
|
||||
function Cash() { sendToCSharp("ChangeCash", [document.getElementById("cash-input").value]); }
|
||||
function SeaSnails() { sendToCSharp("ChangeSeaSnails", [document.getElementById("snails-input").value]); }
|
||||
function execute(cheat) { sendToCSharp(cheat); }
|
||||
function fetchCemuBase() { sendToCSharp("cbase"); }
|
||||
function fetchPids() { sendToCSharp("sessiondata"); }
|
||||
264
modmenu/style.css
Normal file
264
modmenu/style.css
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
:root {
|
||||
--bg-color: #0f0f0f;
|
||||
--card-bg: #181818;
|
||||
--text-color: #d1d1d1;
|
||||
--accent: #bb86fc;
|
||||
--highlight: #03dac6;
|
||||
--border: #2c2c2c;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: 'Inter', system-ui, sans-serif;
|
||||
background-color: var(--bg-color);
|
||||
color: var(--text-color);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding-top: 40px;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.tabs-container {
|
||||
width: 90%;
|
||||
max-width: 700px;
|
||||
background: var(--card-bg);
|
||||
border-radius: 8px;
|
||||
border: 1px solid var(--border);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.tab-menu {
|
||||
display: flex;
|
||||
background: #121212;
|
||||
border-bottom: 1px solid var(--border);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.tab-link {
|
||||
padding: 10px 5px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
background: none;
|
||||
color: #777;
|
||||
font-size: 11px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
transition: 0.2s;
|
||||
flex: 1;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.tab-link:hover { color: #fff; background: #1a1a1a; }
|
||||
|
||||
.tab-link.active {
|
||||
color: var(--accent);
|
||||
border-bottom: 2px solid var(--accent);
|
||||
}
|
||||
|
||||
.tab-content { display: none; padding: 20px; animation: fadeIn 0.3s; }
|
||||
.tab-content.active { display: block; }
|
||||
|
||||
|
||||
#cbase {
|
||||
font-family: monospace;
|
||||
background: #000;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
color: var(--highlight);
|
||||
border: 1px solid #333;
|
||||
}
|
||||
|
||||
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
||||
|
||||
.profile-card {
|
||||
background: #161616;
|
||||
padding: 15px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
input[type="text"] {
|
||||
background: #000;
|
||||
border: 1px solid #333;
|
||||
color: #fff;
|
||||
padding: 8px 12px;
|
||||
border-radius: 3px;
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
input[type="text"]:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.action-btn {
|
||||
background: var(--accent);
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 8px 15px;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
transition: all 0.2s ease-in-out;
|
||||
}
|
||||
|
||||
.action-btn:hover {
|
||||
background: var(--highlight);
|
||||
color: #000;
|
||||
box-shadow: 0 0 12px var(--highlight);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.action-btn:active {
|
||||
transform: translateY(1px);
|
||||
box-shadow: 0 0 4px var(--highlight);
|
||||
}
|
||||
|
||||
.input-label {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #bbb;
|
||||
white-space: nowrap;
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.input-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.input-row:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.input-label {
|
||||
width: 140px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #bbb;
|
||||
}
|
||||
|
||||
.input-group {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
input[type="color"] {
|
||||
-webkit-appearance: none;
|
||||
border: 1px solid var(--border);
|
||||
background: none;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
cursor: pointer;
|
||||
padding: 0;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
input[type="color"]::-webkit-color-swatch-wrapper {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
input[type="color"]::-webkit-color-swatch {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.hex-display {
|
||||
font-family: monospace;
|
||||
color: #888;
|
||||
font-size: 13px;
|
||||
align-self: center;
|
||||
background: #000;
|
||||
padding: 5px 10px;
|
||||
border-radius: 3px;
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
-webkit-appearance: none; /* Disables native GTK styling */
|
||||
-moz-appearance: none;
|
||||
appearance: none;
|
||||
background: #000;
|
||||
border: 1px solid var(--border);
|
||||
color: #fff;
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.dropdown:focus {
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.dropdown option {
|
||||
background: #1a1a1a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.label-stack {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 140px;
|
||||
}
|
||||
|
||||
.input-note {
|
||||
font-size: 10px;
|
||||
color: var(--accent);
|
||||
opacity: 0.8;
|
||||
margin-top: 2px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.main-title {
|
||||
font-size: 36px;
|
||||
font-weight: 800;
|
||||
color: #fff;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 2px;
|
||||
margin-bottom: 25px;
|
||||
text-shadow: 0 0 15px rgba(187, 134, 252, 0.4);
|
||||
}
|
||||
|
||||
.pid-list {
|
||||
margin-top: 10px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr; /* Two columns */
|
||||
gap: 10px;
|
||||
height: 180px;
|
||||
overflow-y: auto;
|
||||
padding-right: 5px;
|
||||
}
|
||||
|
||||
.pid-card {
|
||||
background: #0a0a0a;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
border-left: 3px solid var(--accent);
|
||||
height: 75px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.pid-card b {
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.pid-list::-webkit-scrollbar { width: 4px; }
|
||||
.pid-list::-webkit-scrollbar-thumb { background: var(--border); border-radius: 10px; }
|
||||
Loading…
Reference in a new issue