From 07229424a42094e58c85f81e2185ca2a8603f033 Mon Sep 17 00:00:00 2001 From: TheEvilPenguin Date: Wed, 11 Jun 2025 18:12:28 +1000 Subject: [PATCH] Minimum viable --- CommunityMapLocation/Api/ApiServer.cs | 141 ++++++++++++++++++ CommunityMapLocation/Api/Endpoint.cs | 24 +++ CommunityMapLocation/Api/IEndpoint.cs | 15 ++ .../Api/Location/LocationEndpoint.cs | 40 +++++ .../Api/Location/LocationResponse.cs | 21 +++ .../CommunityMapLocation.csproj | 120 +++++++++++++++ CommunityMapLocation/CommunityMapLocation.sln | 25 ++++ .../Config/RequestSourceType.cs | 14 ++ CommunityMapLocation/Config/Settings.cs | 34 +++++ CommunityMapLocation/Info.json | 8 + CommunityMapLocation/Main.cs | 53 +++++++ .../Properties/AssemblyInfo.cs | 33 ++++ CommunityMapLocation/build_guide.md | 36 +++++ CommunityMapLocation/lib/.gitkeep | 0 14 files changed, 564 insertions(+) create mode 100644 CommunityMapLocation/Api/ApiServer.cs create mode 100644 CommunityMapLocation/Api/Endpoint.cs create mode 100644 CommunityMapLocation/Api/IEndpoint.cs create mode 100644 CommunityMapLocation/Api/Location/LocationEndpoint.cs create mode 100644 CommunityMapLocation/Api/Location/LocationResponse.cs create mode 100644 CommunityMapLocation/CommunityMapLocation.csproj create mode 100644 CommunityMapLocation/CommunityMapLocation.sln create mode 100644 CommunityMapLocation/Config/RequestSourceType.cs create mode 100644 CommunityMapLocation/Config/Settings.cs create mode 100644 CommunityMapLocation/Info.json create mode 100644 CommunityMapLocation/Main.cs create mode 100644 CommunityMapLocation/Properties/AssemblyInfo.cs create mode 100644 CommunityMapLocation/build_guide.md create mode 100644 CommunityMapLocation/lib/.gitkeep diff --git a/CommunityMapLocation/Api/ApiServer.cs b/CommunityMapLocation/Api/ApiServer.cs new file mode 100644 index 0000000..eabf2b3 --- /dev/null +++ b/CommunityMapLocation/Api/ApiServer.cs @@ -0,0 +1,141 @@ +using CommunityMapLocation.Config; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Text; +using System.Text.Json; +using System.Text.Json.Serialization; +using UnityModManagerNet; + +namespace CommunityMapLocation.Api +{ + public class ApiServer + { + public ApiServer(Settings settings, UnityModManager.ModEntry.ModLogger logger) + { + listener = new HttpListener(); + + serializerOptions = new JsonSerializerOptions() + { + ReferenceHandler = ReferenceHandler.IgnoreCycles, + IncludeFields = true, + WriteIndented = true + }; + + this.settings = settings; + this.logger = logger; + } + + private readonly Settings settings; + private readonly UnityModManager.ModEntry.ModLogger logger; + + private readonly HttpListener listener; + + private readonly JsonSerializerOptions serializerOptions; + + private readonly List endpoints = new List(); + + public void AutoRegister(Assembly assembly = null) + { + if (assembly == null) + { + assembly = Assembly.GetExecutingAssembly(); + } + + foreach (var endpointType in assembly.ExportedTypes.Where(t => !t.IsInterface && !t.IsAbstract && typeof(IEndpoint).IsAssignableFrom(t))) + { + try + { + endpoints.Add((IEndpoint)Activator.CreateInstance(endpointType)); + } + catch (Exception ex) + { + logger.Warning($"Could not register endpoint {endpointType.Name}: {ex}"); + } + } + } + + public void RegisterEndpoint(IEndpoint endpoint) + { + endpoints.Add(endpoint); + } + + public void Start() + { + try + { + listener.Prefixes.Add($"http://localhost:{settings.Port}/"); + listener.Start(); + listener.BeginGetContext(HandleConnection, null); + } + catch (Exception ex) + { + logger.Warning(ex.ToString()); + Stop(); + } + } + + private void HandleConnection(IAsyncResult ar) + { + try + { + var context = listener.EndGetContext(ar); + + listener.BeginGetContext(HandleConnection, listener); + + var request = context.Request; + var response = context.Response; + + var matchedEndpoints = endpoints.Where(e => e.Matches(request.Url) && + (request.HttpMethod == "OPTIONS" || request.HttpMethod.Equals(e.HttpMethod.ToString(), StringComparison.OrdinalIgnoreCase))); + + if (!endpoints.Any()) + { + response.StatusCode = (int)HttpStatusCode.NotFound; + } + else + { + var methods = matchedEndpoints.Select(e => e.HttpMethod).Distinct(); + + response.AppendHeader("Access-Control-Allow-Origin", settings.RequestSourceType == RequestSourceType.All ? "*" : settings.RequestSource); + + if (request.HttpMethod == "OPTIONS") + { + response.AddHeader("Access-Control-Allow-Headers", "Content-Type, Accept, X-Requested-With"); + response.AddHeader("Access-Control-Allow-Methods", String.Join(", ", methods.Select(m => m.ToString().ToUpper()))); + response.AddHeader("Access-Control-Max-Age", "1728000"); + } + else + { + var responseBody = matchedEndpoints.First().Handle(logger, request); + + var buffer = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(responseBody, serializerOptions)); + + response.ContentLength64 = buffer.Length; + response.OutputStream.Write(buffer, 0, buffer.Length); + } + } + + response.OutputStream.Close(); + } + catch (HttpListenerException) + { + // HttpListener is stopping, swallow exception + } + } + + internal void Reload() + { + Stop(); + Start(); + } + + public void Stop() + { + listener.Stop(); + listener.Prefixes.Clear(); + } + } +} diff --git a/CommunityMapLocation/Api/Endpoint.cs b/CommunityMapLocation/Api/Endpoint.cs new file mode 100644 index 0000000..f1179c6 --- /dev/null +++ b/CommunityMapLocation/Api/Endpoint.cs @@ -0,0 +1,24 @@ +using System; +using System.Net; +using System.Net.Http; +using UnityModManagerNet; + +namespace CommunityMapLocation.Api +{ + public abstract class Endpoint : IEndpoint + { + public abstract HttpMethod HttpMethod { get; } + public abstract string RoutePattern { get; } + public bool Matches(Uri uri) + { + return String.Concat(uri.Segments).Equals(RoutePattern, StringComparison.OrdinalIgnoreCase); + } + + public abstract T Handle(UnityModManager.ModEntry.ModLogger logger, HttpListenerRequest request); + + object IEndpoint.Handle(UnityModManager.ModEntry.ModLogger logger, HttpListenerRequest request) + { + return Handle(logger, request); + } + } +} diff --git a/CommunityMapLocation/Api/IEndpoint.cs b/CommunityMapLocation/Api/IEndpoint.cs new file mode 100644 index 0000000..bd4ce8f --- /dev/null +++ b/CommunityMapLocation/Api/IEndpoint.cs @@ -0,0 +1,15 @@ +using System; +using System.Net; +using System.Net.Http; +using UnityModManagerNet; + +namespace CommunityMapLocation.Api +{ + public interface IEndpoint + { + HttpMethod HttpMethod { get; } + string RoutePattern { get; } + bool Matches(Uri uri); + object Handle(UnityModManager.ModEntry.ModLogger logger, HttpListenerRequest request); + } +} diff --git a/CommunityMapLocation/Api/Location/LocationEndpoint.cs b/CommunityMapLocation/Api/Location/LocationEndpoint.cs new file mode 100644 index 0000000..808f0ef --- /dev/null +++ b/CommunityMapLocation/Api/Location/LocationEndpoint.cs @@ -0,0 +1,40 @@ +using DV.HUD; +using DV.OriginShift; +using System.Net; +using System.Net.Http; +using UnityEngine; +using UnityModManagerNet; + +namespace CommunityMapLocation.Api.Location +{ + public class LocationEndpoint : Endpoint + { + public override HttpMethod HttpMethod => HttpMethod.Get; + + public override string RoutePattern => "/Location"; + + public override LocationResponse Handle(UnityModManager.ModEntry.ModLogger logger, HttpListenerRequest request) + { + float angle; + + // If the player is in a loco, we want to get the heading of the loco instead of the play + if (PlayerManager.Car?.IsLoco == true) + { + angle = PlayerManager.Car.transform.rotation.eulerAngles.y; + + // If the loco is in reverse, rotate the direction 180 degrees to match the direction of travel + if (PlayerManager.Car.interior.GetComponentInChildren().TryGetControl(InteriorControlsManager.ControlType.Reverser, out var control) && + control.controlImplBase.Value <= 0.25) + { + angle += 180; + } + } + else + { + angle = PlayerManager.PlayerCamera.transform.rotation.eulerAngles.y; + } + + return new LocationResponse(PlayerManager.PlayerTransform.AbsolutePosition(), angle * Mathf.PI / 180); + } + } +} diff --git a/CommunityMapLocation/Api/Location/LocationResponse.cs b/CommunityMapLocation/Api/Location/LocationResponse.cs new file mode 100644 index 0000000..34b8163 --- /dev/null +++ b/CommunityMapLocation/Api/Location/LocationResponse.cs @@ -0,0 +1,21 @@ +using UnityEngine; + +namespace CommunityMapLocation.Api.Location +{ + public class LocationResponse + { + public LocationResponse(Vector3 position, float rotation) + { + X = position.x; + Y = position.y; + Z = position.z; + + Rotation = rotation; + } + + public float X { get; } + public float Y { get; } + public float Z { get; } + public float Rotation { get; } + } +} diff --git a/CommunityMapLocation/CommunityMapLocation.csproj b/CommunityMapLocation/CommunityMapLocation.csproj new file mode 100644 index 0000000..5cd654f --- /dev/null +++ b/CommunityMapLocation/CommunityMapLocation.csproj @@ -0,0 +1,120 @@ + + + + + Debug + AnyCPU + {0A8392E4-B2FA-4018-BD0C-DD6261F10699} + Library + Properties + CommunityMapLocation + CommunityMapLocation + v4.8 + 512 + true + + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + false + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + false + + + + lib\0Harmony.dll + False + + + lib\Assembly-CSharp.dll + False + + + lib\DV.Common.dll + False + + + lib\DV.OriginShiftInfo.dll + False + + + lib\DV.RailTrack.dll + False + + + lib\DV.UserManagement.dll + False + + + lib\DV.Utils.dll + False + + + + + + + + + + + lib\UnityEngine.dll + False + + + lib\UnityEngine.CoreModule.dll + False + + + lib\UnityEngine.IMGUIModule.dll + False + + + lib\UnityEngine.InputLegacyModule.dll + False + + + lib\UnityEngine.UI.dll + False + + + lib\UnityModManager.dll + False + + + + + + + + + + + + + + + + PreserveNewest + + + + + 9.0.6 + + + + \ No newline at end of file diff --git a/CommunityMapLocation/CommunityMapLocation.sln b/CommunityMapLocation/CommunityMapLocation.sln new file mode 100644 index 0000000..975052f --- /dev/null +++ b/CommunityMapLocation/CommunityMapLocation.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.14.36109.1 d17.14 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CommunityMapLocation", "CommunityMapLocation.csproj", "{0A8392E4-B2FA-4018-BD0C-DD6261F10699}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {0A8392E4-B2FA-4018-BD0C-DD6261F10699}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0A8392E4-B2FA-4018-BD0C-DD6261F10699}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0A8392E4-B2FA-4018-BD0C-DD6261F10699}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0A8392E4-B2FA-4018-BD0C-DD6261F10699}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {044646D0-2953-41CC-A775-C1C2BAECADCD} + EndGlobalSection +EndGlobal diff --git a/CommunityMapLocation/Config/RequestSourceType.cs b/CommunityMapLocation/Config/RequestSourceType.cs new file mode 100644 index 0000000..6a5e0ec --- /dev/null +++ b/CommunityMapLocation/Config/RequestSourceType.cs @@ -0,0 +1,14 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace CommunityMapLocation.Config +{ + public enum RequestSourceType + { + Specific, + All + } +} diff --git a/CommunityMapLocation/Config/Settings.cs b/CommunityMapLocation/Config/Settings.cs new file mode 100644 index 0000000..76560f9 --- /dev/null +++ b/CommunityMapLocation/Config/Settings.cs @@ -0,0 +1,34 @@ +using System; +using System.Runtime.CompilerServices; +using UnityEngine; +using UnityModManagerNet; + +namespace CommunityMapLocation.Config +{ + public class Settings : UnityModManager.ModSettings + { + public int Port { get; set; } = 9090; + public RequestSourceType RequestSourceType { get; set; } = RequestSourceType.Specific; + public string RequestSource { get; set; } = "https://pyronicampt.github.io"; + + public void Draw() + { + GUILayout.BeginVertical(GUILayout.ExpandWidth(false)); + + GUILayout.Label("Listening Port:"); + Port = Int32.TryParse(GUILayout.TextField(Port.ToString()), out var port) ? port : Port; + + GUILayout.Label("Allow connections from websites hosted anywhere, or a specific website?"); + + UnityModManager.UI.ToggleGroup((int)RequestSourceType, Enum.GetNames(typeof(RequestSourceType)), i => RequestSourceType = (RequestSourceType)i); + + if (RequestSourceType == RequestSourceType.Specific) + { + GUILayout.Label("Website:"); + RequestSource = GUILayout.TextField(RequestSource); + } + + GUILayout.EndVertical(); + } + } +} diff --git a/CommunityMapLocation/Info.json b/CommunityMapLocation/Info.json new file mode 100644 index 0000000..2082a65 --- /dev/null +++ b/CommunityMapLocation/Info.json @@ -0,0 +1,8 @@ +{ + "Id":"CommunityMapLocation", + "DisplayName":"Derail Valley Community Map Location", + "Author":"TheEvilPenguin", + "Version":"0.0.1", + "ManagerVersion":"0.31.1.0", + "EntryMethod":"CommunityMapLocation.Main.Load" +} \ No newline at end of file diff --git a/CommunityMapLocation/Main.cs b/CommunityMapLocation/Main.cs new file mode 100644 index 0000000..464a030 --- /dev/null +++ b/CommunityMapLocation/Main.cs @@ -0,0 +1,53 @@ +using CommunityMapLocation.Api; +using CommunityMapLocation.Config; +using UnityModManagerNet; + +namespace CommunityMapLocation +{ + [EnableReloading] + public static class Main + { + public static Settings Settings { get; private set; } + private static ApiServer apiServer; + + static bool Load(UnityModManager.ModEntry modEntry) + { + Settings = Settings.Load(modEntry); + + apiServer = new ApiServer(Settings, modEntry.Logger); + apiServer.AutoRegister(); + + modEntry.OnToggle = OnToggle; + modEntry.OnGUI = OnGUI; + modEntry.OnSaveGUI = OnSaveGUI; + + return true; + } + + public static bool OnToggle(UnityModManager.ModEntry modEntry, bool value) + { + if (value) + { + apiServer.Start(); + } + else + { + apiServer.Stop(); + } + + return true; + } + + static void OnGUI(UnityModManager.ModEntry modEntry) + { + Settings.Draw(); + } + + static void OnSaveGUI(UnityModManager.ModEntry modEntry) + { + Settings.Save(modEntry); + + apiServer.Reload(); + } + } +} diff --git a/CommunityMapLocation/Properties/AssemblyInfo.cs b/CommunityMapLocation/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..d056248 --- /dev/null +++ b/CommunityMapLocation/Properties/AssemblyInfo.cs @@ -0,0 +1,33 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("CommunityMapLocation")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("CommunityMapLocation")] +[assembly: AssemblyCopyright("Copyright © 2025")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("0a8392e4-b2fa-4018-bd0c-dd6261f10699")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/CommunityMapLocation/build_guide.md b/CommunityMapLocation/build_guide.md new file mode 100644 index 0000000..00c4084 --- /dev/null +++ b/CommunityMapLocation/build_guide.md @@ -0,0 +1,36 @@ +Assuming Visual Studio Community: + +1. Add the following to the `lib` folder from `[steam common dir]/Derail Valley/DerailValley_Data/Managed`: +- `Assembly-CSharp.dll` +- `BezierCurves.dll` +- `DV.Common.dll` +- `DV.OriginShiftInfo.dll` +- `DV.RailTrack.dll` +- `DV.UserManagement.dll` +- `DV.Utils.dll` +- `UnityEngine.dll` +- `UnityEngine.UI.dll` +- `UnityEngine.InputLegacyModule.dll` +- `UnityEngine.IMGUIModule.dll` +- `UnityEngine.CoreModule.dll` +- `/UnityModManager/0Harmony.dll` +- `/UnityModManager/UnityModManager.dll` + +2. Build Solution +3. Copy the following files from `bin/Debug/` to `Derail Valley/Mods/CommunityMapLocation` +- `info.json` +- `CommunityMapLocation.dll` +- `Microsoft.Bcl.AsyncInterfaces.dll` +- `System.Buffers.dll` +- `System.IO.Pipelines.dll` +- `System.Memory.dll` +- `System.Numerics.Vectors.dll` +- `System.Runtime.CompilerServices.Unsafe.dll` +- `System.Text.Encodings.Web.dll` +- `System.Text.Json.dll` +- `System.Threading.Tasks.Extensions.dll` +- `System.ValueTuple.dll` + +4. Run game using [Unity Mod Manager](https://www.nexusmods.com/site/mods/21) + +Mod functions are all within the UMM settings screen. \ No newline at end of file diff --git a/CommunityMapLocation/lib/.gitkeep b/CommunityMapLocation/lib/.gitkeep new file mode 100644 index 0000000..e69de29