diff --git a/README.md b/README.md index 76785e55..4b2f8f16 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,12 @@ # RedSync -Multiplayer framework for **Cyberpunk 2077** (v2.31). Rebrand of the Cyberverse rewrite — client plugin, redscript layer, and a .NET game server with a C++ networking core. +> **Direction change:** RedSync is pivoting to a **[CyberpunkMP](https://github.com/tiltedphoques/CyberpunkMP) plugin** +> (SDK-only). Core multiplayer sync (puppets, movement, animation, appearance, vehicles) is owned by CyberpunkMP. +> See [`cyberpunkmp-plugin/`](cyberpunkmp-plugin/) for the plugin scaffold and migration map. +> +> The standalone RedSync UDP server / RED4ext sync stack in this repo is **legacy** and not the path for full player anim. + +Multiplayer-related tooling for **Cyberpunk 2077** (v2.31). Originally a rebrand of the Cyberverse rewrite (client plugin, redscript layer, .NET game server). New work targets the CyberpunkMP plugin model. > [!NOTE] > Active development. A self-contained tester package lives in [`release/`](release/) with auto-detect installers. See [Quick start](#quick-start-for-testers). diff --git a/cyberpunkmp-plugin/.gitignore b/cyberpunkmp-plugin/.gitignore new file mode 100644 index 00000000..9451e554 --- /dev/null +++ b/cyberpunkmp-plugin/.gitignore @@ -0,0 +1,6 @@ +**/bin/ +**/obj/ +local-plugins/ +**/admins.json +**/redsync.json +!RedSyncSystem/redsync.json diff --git a/cyberpunkmp-plugin/MIGRATION.md b/cyberpunkmp-plugin/MIGRATION.md new file mode 100644 index 00000000..85db2449 --- /dev/null +++ b/cyberpunkmp-plugin/MIGRATION.md @@ -0,0 +1,49 @@ +# Migration map: RedSync host → CyberpunkMP plugin + +## Product decision + +| Before | After | +|--------|--------| +| RedSync = full multiplayer stack | CyberpunkMP = multiplayer stack | +| RedSync.Server + RED4ext netcode | Abandoned as competing host | +| Jackie/Judy + teleport puppets | CMP puppets / anim / appearance | +| Custom UDP protocol 0x7/0x8 | CMP protobuf + RPC | + +## Feature-by-feature + +| RedSync piece | Action | +|---------------|--------| +| `shared/protocol`, native GNS, `NetworkGameSystem` sync | **Drop** — CMP replaces | +| Player anim flags / TPP research | **Drop** — CMP core | +| `server/Managed` GameServer | **Freeze** — reference only | +| `server/Managed/Plugins` (.NET host we added) | **Superseded** by CMP plugin loader | +| Admin (`admins.json`, F7, kick/ban) | **Port** into `RedSyncSystem` + CMP admin/web if useful | +| Discord webhook | **Port** into `RedSyncSystem` | +| Spawn anchor | **Evaluate** — port only if CMP lacks equivalent | +| Tester `release/` RedSync client | **Retargeted** — `Setup-CyberpunkMP.bat` / `CyberpunkMP - Play.bat` + `CONNECT-CMP.txt` | +| Redscript combat / nametags | **Keep ideas**; reimplement only as CMP client plugins via RPC | + +## Plugin conventions (CMP) + +From CMP’s loader: + +- Directory: `plugins/System/` +- DLL: `System.dll` +- Entry type: `System.Plugin` +- Static ctor runs at load; wire `Server.PlayerSystem` / `Server.World` events there +- RPC: redscript `ServerRpc` / `ClientRpc` + generated C# (`SdkGenerator`); implement `*_Impl` partials +- Optional: `IWebApiHook` + `assets/` for admin UI widgets + +## Suggested delivery order + +1. **Scaffold loads** — `RedSyncSystem` logs on load; `plugins` list shows it (CMP admin). +2. **Join/leave Discord (or console) announce** — uses `PlayerJoinEvent` / `PlayerLeftEvent`. +3. **Admin helpers** — thin RPCs for kick/say if CMP API exposes enough. +4. **Client redscript** — only for UX that CMP doesn’t ship. +5. **Retire** standalone RedSync server docs from the “how to play” path. + +## Explicit non-goals + +- Do not vendor CyberpunkMP native client, archives, or animation controllers. +- Do not reintroduce a second netcode stack beside CMP. +- Do not ship CMP binaries inside this repo. diff --git a/cyberpunkmp-plugin/README.md b/cyberpunkmp-plugin/README.md new file mode 100644 index 00000000..e7f7a05c --- /dev/null +++ b/cyberpunkmp-plugin/README.md @@ -0,0 +1,83 @@ +# RedSync → CyberpunkMP plugin pivot + +RedSync is **no longer developing a competing multiplayer host**. +Player sync (movement, animation, appearance, vehicles) is handled by +**[CyberpunkMP](https://github.com/tiltedphoques/CyberpunkMP)**. + +This folder is the start of RedSync as a **CyberpunkMP plugin** that uses only +their published **.NET SDK** + **redscript RPC** surfaces. + +## License (read this) + +CyberpunkMP’s license allows plugins that: + +- use the provided SDK / interfaces +- do **not** modify CyberpunkMP source +- do **not** redistribute CyberpunkMP itself + +Example plugins under `code/scripting/EmoteSystem`, `JobSystem`, and +`code/assets/redscript/Plugins` are MIT. Study those patterns; do **not** copy +CMP core (animation controllers, archives, native client). + +## What CMP already owns (do not rebuild) + +| Area | Owner | +|------|--------| +| Connection / world | CyberpunkMP | +| Remote puppets + appearance | CyberpunkMP | +| Movement + basic animation | CyberpunkMP | +| Vehicles | CyberpunkMP | +| RPC transport | CyberpunkMP | + +## What RedSync plugin can add + +| Feature | Notes | +|---------|--------| +| Discord join/leave / server log webhook | Port from RedSync `DiscordWebhookTarget` | +| Admin allowlist extras | Kick/ban/announce UX on top of CMP | +| Spawn-anchor style conveniences | If still useful alongside CMP | +| RP / economy / jobs-style gameplay | Same shape as CMP `JobSystem` | +| Custom F10-style UI | Client redscript + `ServerRpc` / `ClientRpc` | + +## Layout + +``` +cyberpunkmp-plugin/ + README.md ← this file + MIGRATION.md ← feature migration map + RedSyncSystem/ ← server plugin (loads as plugins/RedSyncSystem/) + client-redscript/ ← optional client RPC / UI scripts for CMP +``` + +## Build / install (against a CMP server tree) + +1. Build or download a CyberpunkMP server that includes `CyberpunkSdk`. +2. Set `CYBERPUNKMP_SDK` to the folder containing `CyberpunkSdk.dll` + (or leave unset to compile against local **SdkStubs** for CI only — do not + deploy stub `CyberpunkSdk.dll` into a live CMP server). +3. Build: + +```bash +dotnet build cyberpunkmp-plugin/RedSyncSystem -c Release +``` + +4. Install **only** the plugin assembly: + +```text +/plugins/RedSyncSystem/RedSyncSystem.dll +``` + +Loader rules (from CMP): folder name `RedSyncSystem`, assembly +`RedSyncSystem.dll`, type `RedSyncSystem.Plugin`, static constructor runs on load. + +5. Optional: set `REDSYNC_DISCORD_WEBHOOK` on the CMP server process for join/leave announces. + +## Local build (this machine) + +```bash +./cyberpunkmp-plugin/build-local.sh +dotnet run --project cyberpunkmp-plugin/RedSyncSystem.Smoke -c Release +``` + +Stages `cyberpunkmp-plugin/local-plugins/RedSyncSystem/` (DLL + `redsync.json`). +Copy that folder into a CyberpunkMP server’s `plugins/` directory to load it. diff --git a/cyberpunkmp-plugin/RedSyncSystem.Smoke/Program.cs b/cyberpunkmp-plugin/RedSyncSystem.Smoke/Program.cs new file mode 100644 index 00000000..2f46a7a9 --- /dev/null +++ b/cyberpunkmp-plugin/RedSyncSystem.Smoke/Program.cs @@ -0,0 +1,20 @@ +using CyberpunkSdk; +using RedSyncSystem; + +internal static class Program +{ + private static void Main() + { + Console.WriteLine("RedSyncSystem local smoke test"); + _ = Plugin.Instance; + Console.WriteLine($"Config: {PluginConfig.ConfigPath}"); + + StubPlayerSystem.Instance.SimulateJoin(1, "LocalTester"); + StubPlayerSystem.Instance.SimulateLeave(1); + StubWorld.Instance.SimulateTick(0.1f); + + Plugin.Instance.Admins.Add("LocalTester"); + Console.WriteLine($"IsAdmin(LocalTester)={Plugin.Instance.IsAdmin("LocalTester")}"); + Console.WriteLine("Smoke test OK"); + } +} diff --git a/cyberpunkmp-plugin/RedSyncSystem.Smoke/Program.csproj b/cyberpunkmp-plugin/RedSyncSystem.Smoke/Program.csproj new file mode 100644 index 00000000..403c2d83 --- /dev/null +++ b/cyberpunkmp-plugin/RedSyncSystem.Smoke/Program.csproj @@ -0,0 +1,13 @@ + + + Exe + net8.0 + enable + enable + 12 + + + + + + diff --git a/cyberpunkmp-plugin/RedSyncSystem/AdminAllowlist.cs b/cyberpunkmp-plugin/RedSyncSystem/AdminAllowlist.cs new file mode 100644 index 00000000..6aaae5f4 --- /dev/null +++ b/cyberpunkmp-plugin/RedSyncSystem/AdminAllowlist.cs @@ -0,0 +1,131 @@ +using System.Text.Json; + +namespace RedSyncSystem; + +/// +/// Optional RedSync-side admin allowlist (usernames). CMP may have its own admin; +/// this list is for RedSync plugin features only. +/// +public sealed class AdminAllowlist +{ + private readonly object _lock = new(); + private readonly string _path; + private readonly LoggerProxy _log; + private HashSet _admins = new(StringComparer.OrdinalIgnoreCase); + + private static readonly JsonSerializerOptions JsonOptions = new() + { + WriteIndented = true, + PropertyNameCaseInsensitive = true + }; + + public AdminAllowlist(string pluginDirectory, IEnumerable? seed, LoggerProxy log) + { + _log = log; + _path = Path.Combine(pluginDirectory, "admins.json"); + Load(seed); + } + + public bool IsAdmin(string? name) + { + if (string.IsNullOrWhiteSpace(name)) + { + return false; + } + + lock (_lock) + { + return _admins.Contains(name.Trim()); + } + } + + public bool Add(string name) + { + name = name.Trim(); + if (string.IsNullOrWhiteSpace(name)) + { + return false; + } + + lock (_lock) + { + if (!_admins.Add(name)) + { + return false; + } + + SaveLocked(); + return true; + } + } + + public bool Remove(string name) + { + name = name.Trim(); + lock (_lock) + { + if (!_admins.Remove(name)) + { + return false; + } + + SaveLocked(); + return true; + } + } + + public IReadOnlyList List() + { + lock (_lock) + { + return _admins.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList(); + } + } + + private void Load(IEnumerable? seed) + { + lock (_lock) + { + _admins = new HashSet(StringComparer.OrdinalIgnoreCase); + if (File.Exists(_path)) + { + try + { + var names = JsonSerializer.Deserialize>(File.ReadAllText(_path), JsonOptions) + ?? []; + foreach (var n in names) + { + if (!string.IsNullOrWhiteSpace(n)) + { + _admins.Add(n.Trim()); + } + } + } + catch (Exception ex) + { + _log.Warn($"admins.json load failed: {ex.Message}"); + } + } + + if (seed != null) + { + foreach (var n in seed) + { + if (!string.IsNullOrWhiteSpace(n)) + { + _admins.Add(n.Trim()); + } + } + } + + SaveLocked(); + _log.Info($"Admin allowlist: {_admins.Count} name(s) ({_path})"); + } + } + + private void SaveLocked() + { + var list = _admins.OrderBy(x => x, StringComparer.OrdinalIgnoreCase).ToList(); + File.WriteAllText(_path, JsonSerializer.Serialize(list, JsonOptions)); + } +} diff --git a/cyberpunkmp-plugin/RedSyncSystem/DiscordWebhook.cs b/cyberpunkmp-plugin/RedSyncSystem/DiscordWebhook.cs new file mode 100644 index 00000000..3c9ac387 --- /dev/null +++ b/cyberpunkmp-plugin/RedSyncSystem/DiscordWebhook.cs @@ -0,0 +1,114 @@ +using System.Collections.Concurrent; +using System.Text; +using System.Text.Json; + +namespace RedSyncSystem; + +/// +/// Batched Discord webhook sender (ported from RedSync server DiscordWebhookTarget ideas). +/// +public sealed class DiscordWebhook : IDisposable +{ + private const int FlushIntervalMs = 3000; + private const int MaxContentLength = 1900; + private const int MaxQueuedLines = 500; + + private readonly string _webhookUrl; + private readonly HttpClient _http = new() { Timeout = TimeSpan.FromSeconds(8) }; + private readonly ConcurrentQueue _pending = new(); + private readonly Timer _timer; + private readonly LoggerProxy _log; + private int _flushing; + private bool _disabled; + + public bool Enabled => !_disabled && !string.IsNullOrWhiteSpace(_webhookUrl); + + public DiscordWebhook(string webhookUrl, LoggerProxy log) + { + _log = log; + if (string.IsNullOrWhiteSpace(webhookUrl) + || string.Equals(webhookUrl, "off", StringComparison.OrdinalIgnoreCase)) + { + _disabled = true; + _webhookUrl = ""; + _timer = new Timer(_ => { }, null, Timeout.Infinite, Timeout.Infinite); + return; + } + + _webhookUrl = webhookUrl.Trim(); + _timer = new Timer(_ => _ = FlushAsync(), null, FlushIntervalMs, FlushIntervalMs); + _log.Info("Discord webhook enabled."); + } + + public void Enqueue(string line) + { + if (!Enabled || string.IsNullOrWhiteSpace(line)) + { + return; + } + + while (_pending.Count >= MaxQueuedLines && _pending.TryDequeue(out _)) + { + } + + _pending.Enqueue(line.Trim()); + } + + private async Task FlushAsync() + { + if (!Enabled || Interlocked.CompareExchange(ref _flushing, 1, 0) != 0) + { + return; + } + + try + { + while (!_pending.IsEmpty) + { + var batch = new StringBuilder(); + while (_pending.TryPeek(out var line)) + { + if (line.Length > MaxContentLength) + { + line = line[..MaxContentLength]; + } + + if (batch.Length + line.Length + 1 > MaxContentLength) + { + break; + } + + _pending.TryDequeue(out _); + batch.Append(line).Append('\n'); + } + + if (batch.Length == 0) + { + return; + } + + var payload = JsonSerializer.Serialize(new { content = batch.ToString() }); + using var body = new StringContent(payload, Encoding.UTF8, "application/json"); + var response = await _http.PostAsync(_webhookUrl, body).ConfigureAwait(false); + if (!response.IsSuccessStatusCode) + { + _log.Warn($"Discord webhook HTTP {(int)response.StatusCode}"); + } + } + } + catch (Exception ex) + { + _log.Warn($"Discord webhook flush failed: {ex.Message}"); + } + finally + { + Interlocked.Exchange(ref _flushing, 0); + } + } + + public void Dispose() + { + _timer.Dispose(); + _http.Dispose(); + } +} diff --git a/cyberpunkmp-plugin/RedSyncSystem/Plugin.cs b/cyberpunkmp-plugin/RedSyncSystem/Plugin.cs new file mode 100644 index 00000000..df372cc2 --- /dev/null +++ b/cyberpunkmp-plugin/RedSyncSystem/Plugin.cs @@ -0,0 +1,129 @@ +using CyberpunkSdk; + +namespace RedSyncSystem; + +/// +/// CyberpunkMP server plugin entry. Loaded from plugins/RedSyncSystem/RedSyncSystem.dll +/// as type RedSyncSystem.Plugin (CMP loader convention). +/// +/// Does not implement multiplayer sync — that is CyberpunkMP core. +/// +public class Plugin +{ + public static Plugin Instance { get; private set; } = null!; + + private readonly LoggerProxy _log = new("RedSync"); + private readonly object _gate = new(); + private bool _hooksAttached; + + private PluginConfig _config = new(); + private DiscordWebhook? _discord; + private AdminAllowlist? _admins; + private readonly Dictionary _names = new(); + + public AdminAllowlist Admins => _admins ?? throw new InvalidOperationException("Plugin not initialized"); + public PluginConfig Config => _config; + + static Plugin() + { + Instance = new Plugin(); + } + + private Plugin() + { + try + { + var pluginDir = AppContext.BaseDirectory; + Directory.CreateDirectory(pluginDir); + + _config = PluginConfig.LoadOrCreate(_log); + _discord = new DiscordWebhook(_config.DiscordWebhook, _log); + _admins = new AdminAllowlist(pluginDir, _config.Admins, _log); + + AttachHooks(); + _log.Info("RedSyncSystem loaded (CMP plugin). Sync owned by CyberpunkMP core."); + _discord.Enqueue("**RedSyncSystem** plugin online."); + } + catch (Exception ex) + { + try + { + _log.Error($"RedSyncSystem failed to initialize: {ex}"); + } + catch + { + Console.Error.WriteLine($"RedSyncSystem failed to initialize: {ex}"); + } + } + } + + private void AttachHooks() + { + lock (_gate) + { + if (_hooksAttached) + { + return; + } + + Server.PlayerSystem.PlayerJoinEvent += OnPlayerJoin; + Server.PlayerSystem.PlayerLeftEvent += OnPlayerLeft; + Server.World.UpdateEvent += OnWorldUpdate; + _hooksAttached = true; + } + } + + private void OnPlayerJoin(ulong id) + { + try + { + var player = Server.PlayerSystem.GetById(id); + lock (_gate) + { + _names[id] = player.Username; + } + + _log.Info($"Player joined: {player.Username} (id={id}, puppet={player.PuppetId})"); + if (_config.AnnounceJoins) + { + _discord?.Enqueue($"**{player.Username}** joined the server."); + } + } + catch (Exception ex) + { + _log.Error($"OnPlayerJoin failed: {ex.Message}"); + } + } + + private void OnPlayerLeft(ulong id) + { + try + { + string name; + lock (_gate) + { + if (!_names.Remove(id, out name!)) + { + name = $"id:{id}"; + } + } + + _log.Info($"Player left: {name} (id={id})"); + if (_config.AnnounceLeaves) + { + _discord?.Enqueue($"**{name}** left the server."); + } + } + catch (Exception ex) + { + _log.Error($"OnPlayerLeft failed: {ex.Message}"); + } + } + + private void OnWorldUpdate(float delta) + { + // Reserved for lightweight RedSync extras. + } + + public bool IsAdmin(string? username) => _admins?.IsAdmin(username) == true; +} diff --git a/cyberpunkmp-plugin/RedSyncSystem/PluginConfig.cs b/cyberpunkmp-plugin/RedSyncSystem/PluginConfig.cs new file mode 100644 index 00000000..b47c10d9 --- /dev/null +++ b/cyberpunkmp-plugin/RedSyncSystem/PluginConfig.cs @@ -0,0 +1,125 @@ +using System.Text.Json; + +namespace RedSyncSystem; + +public sealed class PluginConfig +{ + public string DiscordWebhook { get; set; } = "off"; + public bool AnnounceJoins { get; set; } = true; + public bool AnnounceLeaves { get; set; } = true; + public string[] Admins { get; set; } = []; + + private static readonly JsonSerializerOptions JsonOptions = new() + { + WriteIndented = true, + PropertyNameCaseInsensitive = true, + ReadCommentHandling = JsonCommentHandling.Skip, + AllowTrailingCommas = true + }; + + public static string ConfigPath => + Path.Combine(AppContext.BaseDirectory, "redsync.json"); + + public static PluginConfig LoadOrCreate(LoggerProxy log) + { + // Env wins over file for Discord (matches legacy RedSync server). + var envWebhook = Environment.GetEnvironmentVariable("REDSYNC_DISCORD_WEBHOOK"); + + try + { + if (!File.Exists(ConfigPath)) + { + var fresh = new PluginConfig(); + File.WriteAllText(ConfigPath, JsonSerializer.Serialize(fresh, JsonOptions)); + log.Info($"Wrote default config: {ConfigPath}"); + if (!string.IsNullOrWhiteSpace(envWebhook)) + { + fresh.DiscordWebhook = envWebhook; + } + + return fresh; + } + + var loaded = JsonSerializer.Deserialize(File.ReadAllText(ConfigPath), JsonOptions) + ?? new PluginConfig(); + if (!string.IsNullOrWhiteSpace(envWebhook)) + { + loaded.DiscordWebhook = envWebhook; + } + + return loaded; + } + catch (Exception ex) + { + log.Warn($"Failed to load redsync.json ({ex.Message}); using defaults."); + var fallback = new PluginConfig(); + if (!string.IsNullOrWhiteSpace(envWebhook)) + { + fallback.DiscordWebhook = envWebhook; + } + + return fallback; + } + } + + public void Save() + { + File.WriteAllText(ConfigPath, JsonSerializer.Serialize(this, JsonOptions)); + } +} + +/// Thin wrapper so config can log before CyberpunkSdk.Logger exists in odd load orders. +public sealed class LoggerProxy +{ + private readonly CyberpunkSdk.Systems.Logger? _sdk; + private readonly string _name; + + public LoggerProxy(string name) + { + _name = name; + try + { + _sdk = new CyberpunkSdk.Systems.Logger(name); + } + catch + { + _sdk = null; + } + } + + public void Info(string msg) + { + if (_sdk != null) + { + _sdk.Info(msg); + } + else + { + Console.WriteLine($"[INF:{_name}] {msg}"); + } + } + + public void Warn(string msg) + { + if (_sdk != null) + { + _sdk.Warn(msg); + } + else + { + Console.WriteLine($"[WRN:{_name}] {msg}"); + } + } + + public void Error(string msg) + { + if (_sdk != null) + { + _sdk.Error(msg); + } + else + { + Console.Error.WriteLine($"[ERR:{_name}] {msg}"); + } + } +} diff --git a/cyberpunkmp-plugin/RedSyncSystem/RedSyncSystem.csproj b/cyberpunkmp-plugin/RedSyncSystem/RedSyncSystem.csproj new file mode 100644 index 00000000..fe6b9cda --- /dev/null +++ b/cyberpunkmp-plugin/RedSyncSystem/RedSyncSystem.csproj @@ -0,0 +1,42 @@ + + + + net8.0 + enable + enable + RedSyncSystem + RedSyncSystem + 12 + + + + + PreserveNewest + + + + + + $(CYBERPUNKMP_SDK) + + + + + + + + + $(CyberpunkMpSdkDir)\CyberpunkSdk.dll + false + + + + + + + + diff --git a/cyberpunkmp-plugin/RedSyncSystem/redsync.json b/cyberpunkmp-plugin/RedSyncSystem/redsync.json new file mode 100644 index 00000000..ad7947e7 --- /dev/null +++ b/cyberpunkmp-plugin/RedSyncSystem/redsync.json @@ -0,0 +1,6 @@ +{ + "DiscordWebhook": "off", + "AnnounceJoins": true, + "AnnounceLeaves": true, + "Admins": [] +} diff --git a/cyberpunkmp-plugin/SdkStubs/CyberpunkSdk.Stubs.cs b/cyberpunkmp-plugin/SdkStubs/CyberpunkSdk.Stubs.cs new file mode 100644 index 00000000..747b6d53 --- /dev/null +++ b/cyberpunkmp-plugin/SdkStubs/CyberpunkSdk.Stubs.cs @@ -0,0 +1,107 @@ +// Compile-time stubs for CyberpunkSdk public surface. +// Production builds must reference the real CyberpunkSdk from a CyberpunkMP server. + +using CyberpunkSdk.Game; +using CyberpunkSdk.Systems; + +namespace CyberpunkSdk +{ + public static class Server + { + public static PlayerSystem PlayerSystem { get; set; } = StubPlayerSystem.Instance; + public static World World { get; set; } = StubWorld.Instance; + public static IRpcManager RpcManager { get; set; } = new StubRpcManager(); + } + + public sealed class StubPlayerSystem : PlayerSystem + { + public static StubPlayerSystem Instance { get; } = new(); + private readonly Dictionary _players = new(); + + public override IEnumerable PlayerIds => _players.Keys; + + public override Player GetById(ulong Id) => + _players.TryGetValue(Id, out var p) + ? p + : throw new KeyNotFoundException($"No stub player {Id}"); + + public void SimulateJoin(ulong id, string username) + { + _players[id] = new Player + { + Id = id, + PuppetId = id + 1000, + ConnectionId = (uint)id, + Username = username + }; + OnPlayerJoin(id); + } + + public void SimulateLeave(ulong id) + { + _players.Remove(id); + OnPlayerLeft(id); + } + } + + public sealed class StubWorld : World + { + public static StubWorld Instance { get; } = new(); + public void SimulateTick(float delta) => OnUpdate(delta); + } + + file sealed class StubRpcManager : IRpcManager + { + public void Call(ulong playerId, ulong klass, ulong func, object args) + { + } + } +} + +namespace CyberpunkSdk.Systems +{ + public class Logger + { + private readonly string _name; + public Logger(string name) => _name = name; + public void Debug(string aMessage) => Console.WriteLine($"[DBG:{_name}] {aMessage}"); + public void Info(string aMessage) => Console.WriteLine($"[INF:{_name}] {aMessage}"); + public void Warn(string aMessage) => Console.WriteLine($"[WRN:{_name}] {aMessage}"); + public void Error(string aMessage) => Console.Error.WriteLine($"[ERR:{_name}] {aMessage}"); + } + + public interface IRpcManager + { + void Call(ulong playerId, ulong klass, ulong func, object args); + } +} + +namespace CyberpunkSdk.Game +{ + public abstract class PlayerSystem + { + public delegate void PlayerEvent(ulong Id); + public event PlayerEvent? PlayerJoinEvent; + public event PlayerEvent? PlayerLeftEvent; + public abstract IEnumerable PlayerIds { get; } + public abstract Player GetById(ulong Id); + protected void OnPlayerJoin(ulong Id) => PlayerJoinEvent?.Invoke(Id); + protected void OnPlayerLeft(ulong Id) => PlayerLeftEvent?.Invoke(Id); + } + + public abstract class World + { + public delegate void UpdateDelegate(float Delta); + public event UpdateDelegate? UpdateEvent; + protected void OnUpdate(float Delta) => UpdateEvent?.Invoke(Delta); + } + + public class Player + { + public ulong Id { get; init; } + public ulong PuppetId { get; init; } + public uint ConnectionId { get; init; } + public string Username { get; init; } = ""; + public void SendChat(string From, string Message) { } + } +} diff --git a/cyberpunkmp-plugin/SdkStubs/CyberpunkSdk.Stubs.csproj b/cyberpunkmp-plugin/SdkStubs/CyberpunkSdk.Stubs.csproj new file mode 100644 index 00000000..acb085ab --- /dev/null +++ b/cyberpunkmp-plugin/SdkStubs/CyberpunkSdk.Stubs.csproj @@ -0,0 +1,10 @@ + + + net8.0 + enable + enable + CyberpunkSdk + CyberpunkSdk + 12 + + diff --git a/cyberpunkmp-plugin/build-local.sh b/cyberpunkmp-plugin/build-local.sh new file mode 100755 index 00000000..0b809165 --- /dev/null +++ b/cyberpunkmp-plugin/build-local.sh @@ -0,0 +1,20 @@ +#!/usr/bin/env bash +# Build RedSyncSystem locally and stage a CMP-style plugins/ folder. +set -euo pipefail +ROOT="$(cd "$(dirname "$0")" && pwd)" +OUT="${ROOT}/local-plugins/RedSyncSystem" +export PATH="${HOME}/.dotnet:${PATH}" + +echo "==> Building RedSyncSystem (stubs unless CYBERPUNKMP_SDK is set)" +dotnet build "${ROOT}/RedSyncSystem/RedSyncSystem.csproj" -c Release --nologo + +echo "==> Staging ${OUT}" +rm -rf "${OUT}" +mkdir -p "${OUT}" +cp -f "${ROOT}/RedSyncSystem/bin/Release/net8.0/RedSyncSystem.dll" "${OUT}/" +# Do NOT copy stub CyberpunkSdk.dll — live CMP already provides it. +cp -f "${ROOT}/RedSyncSystem/redsync.json" "${OUT}/redsync.json" + +echo "==> Staged:" +ls -la "${OUT}" +echo "Install: copy ${OUT} to /plugins/RedSyncSystem/" diff --git a/cyberpunkmp-plugin/client-redscript/RedSyncCmp.reds b/cyberpunkmp-plugin/client-redscript/RedSyncCmp.reds new file mode 100644 index 00000000..444f493b --- /dev/null +++ b/cyberpunkmp-plugin/client-redscript/RedSyncCmp.reds @@ -0,0 +1,28 @@ +// RedSync client extras for CyberpunkMP. +// +// Place alongside CyberpunkMP redscript (or your CMP client plugin pack). +// RPC class/function hashes must be generated with CMP's SdkGenerator once +// ServerRpc natives are declared — until then this file is a stub. +// +// module CyberpunkMP.Plugins +// +// import CyberpunkMP.* +// +// // Example shape (uncomment after SdkGenerator run): +// // public native class RedSyncServer extends ServerRpc { +// // public static native func Ping() -> Void; +// // } +// // +// // public class RedSyncClient extends ClientRpc { +// // public func Notify(text: String) -> Void { +// // FTLog(s"[RedSync] \(text)"); +// // } +// // } + +module RedSync.CyberpunkMP + +public class RedSyncCmpClientStub { + public static func Describe() -> String { + return "RedSync runs as a CyberpunkMP plugin; core sync is CMP."; + } +} diff --git a/cyberpunkmp-plugin/run-cmp-server.sh b/cyberpunkmp-plugin/run-cmp-server.sh new file mode 100755 index 00000000..b2c3578d --- /dev/null +++ b/cyberpunkmp-plugin/run-cmp-server.sh @@ -0,0 +1,22 @@ +#!/usr/bin/env bash +# Run the local CyberpunkMP server build with RedSyncSystem staged. +set -euo pipefail +CMP_ROOT="${CMP_ROOT:-/tmp/CyberpunkMP/build/linux/x86_64/release}" +PLUGIN_SRC="$(cd "$(dirname "$0")" && pwd)/local-plugins/RedSyncSystem" + +if [[ ! -x "$CMP_ROOT/Server.Loader" ]]; then + echo "Missing $CMP_ROOT/Server.Loader — build CyberpunkMP first (xmake build Server.Loader)." >&2 + exit 1 +fi + +export PATH="${HOME}/.dotnet:${PATH}" +export DOTNET_ROOT="${HOME}/.dotnet" +export LD_LIBRARY_PATH="${CMP_ROOT}:${LD_LIBRARY_PATH:-}" +export CYBERPUNKMP_ADMIN_USERNAME="${CYBERPUNKMP_ADMIN_USERNAME:-admin}" +export CYBERPUNKMP_ADMIN_PASSWORD="${CYBERPUNKMP_ADMIN_PASSWORD:-redsync-local}" + +mkdir -p "$CMP_ROOT/plugins/RedSyncSystem" +cp -f "$PLUGIN_SRC/RedSyncSystem.dll" "$PLUGIN_SRC/redsync.json" "$CMP_ROOT/plugins/RedSyncSystem/" + +cd "$CMP_ROOT" +exec ./Server.Loader "$@" diff --git a/release/CONNECT-CMP.txt b/release/CONNECT-CMP.txt new file mode 100644 index 00000000..0375da97 --- /dev/null +++ b/release/CONNECT-CMP.txt @@ -0,0 +1,27 @@ +CyberpunkMP connect (cloud server) +================================= + +SERVER: 147.185.221.225 port 29309 + +CRITICAL: Input Loader loads TWO CyberpunkMP.xml files. +The plugin copy OVERRIDES r6\input. You must patch BOTH: + + 1) ...\r6\input\CyberpunkMP.xml + 2) ...\red4ext\plugins\zzzCyberpunkMP\assets\Inputs\CyberpunkMP.xml + +Paste this to remap Connect to F6 in BOTH files: + +$files = @( + "C:\Program Files (x86)\Steam\steamapps\common\Cyberpunk 2077\r6\input\CyberpunkMP.xml", + "C:\Program Files (x86)\Steam\steamapps\common\Cyberpunk 2077\red4ext\plugins\zzzCyberpunkMP\assets\Inputs\CyberpunkMP.xml" +) +$utf8 = New-Object System.Text.UTF8Encoding $false +foreach ($xml in $files) { + $c = (Get-Content -Raw $xml).Replace('IK_Slash','IK_F6') + [IO.File]::WriteAllText($xml, $c, $utf8) + Select-String -Path $xml -Pattern 'IK_F6|IK_Slash' +} + +Launch: + --online --ip=147.185.221.225 --port=29309 +Hold F6 in Night City. diff --git a/release/Click-Test-Connect.ps1 b/release/Click-Test-Connect.ps1 new file mode 100644 index 00000000..4b72948f --- /dev/null +++ b/release/Click-Test-Connect.ps1 @@ -0,0 +1,68 @@ +#Requires -Version 5.1 +# Clicks CyberpunkMP ImGui "Test" -> "Connect" without needing a working mouse in-game. +# Run WHILE the game is in Night City with the Test menu visible and CMP Relay open. +$ErrorActionPreference = "Stop" + +Add-Type @" +using System; +using System.Runtime.InteropServices; +public class CmpClick { + [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr hWnd); + [DllImport("user32.dll")] public static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); + [DllImport("user32.dll")] public static extern bool SetCursorPos(int X, int Y); + [DllImport("user32.dll")] public static extern void mouse_event(int dwFlags, int dx, int dy, int cButtons, int dwExtraInfo); + [DllImport("user32.dll")] public static extern bool ClientToScreen(IntPtr hWnd, ref POINT lpPoint); + [DllImport("user32.dll")] public static extern bool ShowCursor(bool bShow); + public const int MOUSEEVENTF_LEFTDOWN = 0x0002; + public const int MOUSEEVENTF_LEFTUP = 0x0004; + [StructLayout(LayoutKind.Sequential)] public struct RECT { public int Left, Top, Right, Bottom; } + [StructLayout(LayoutKind.Sequential)] public struct POINT { public int X, Y; } + public static void Click(int x, int y) { + SetCursorPos(x, y); + System.Threading.Thread.Sleep(80); + mouse_event(MOUSEEVENTF_LEFTDOWN, 0, 0, 0, 0); + System.Threading.Thread.Sleep(40); + mouse_event(MOUSEEVENTF_LEFTUP, 0, 0, 0, 0); + } +} +"@ + +$p = Get-Process Cyberpunk2077 -ErrorAction SilentlyContinue | Select-Object -First 1 +if (-not $p) { throw "Cyberpunk2077 is not running. Launch PLAY.bat first, get into Night City with Test menu visible." } + +$hwnd = $p.MainWindowHandle +if ($hwnd -eq [IntPtr]::Zero) { throw "No main window handle." } + +[void][CmpClick]::SetForegroundWindow($hwnd) +Start-Sleep -Milliseconds 400 + +$rect = New-Object CmpClick+RECT +[void][CmpClick]::GetWindowRect($hwnd, [ref]$rect) +$left = $rect.Left +$top = $rect.Top +$width = $rect.Right - $rect.Left +Write-Host "Window: $left,$top size=$width" + +# ImGui main menu bar is at the top. "Test" is usually near the left. +# Try several X positions along the top bar. +$ys = @(($top + 12), ($top + 18), ($top + 24)) +$xs = @( + ($left + 20), ($left + 35), ($left + 50), ($left + 70), ($left + 100), + ($left + [int]($width * 0.05)), ($left + [int]($width * 0.08)) +) + +Write-Host "Clicking likely Test menu positions, then Connect dropdown..." +foreach ($y in $ys) { + foreach ($x in $xs) { + [CmpClick]::Click($x, $y) + Start-Sleep -Milliseconds 120 + # Connect button appears just below Test in the dropdown + [CmpClick]::Click($x, ($y + 28)) + Start-Sleep -Milliseconds 80 + [CmpClick]::Click($x, ($y + 40)) + Start-Sleep -Milliseconds 80 + } +} + +Write-Host "DONE clicking. Check the CMP Relay window for game->cloud packets." +Write-Host "If still zero, say so." diff --git a/release/CmpConnect.ps1 b/release/CmpConnect.ps1 new file mode 100644 index 00000000..e15876c4 --- /dev/null +++ b/release/CmpConnect.ps1 @@ -0,0 +1,198 @@ +#Requires -Version 5.1 +# Shared CMP connect helpers for launch + Steam setup. + +function Read-CmpServerConfig { + param([string]$ReleaseRoot) + + $hostName = "sites-avon.gl.at.ply.gg" + $port = 29309 + + $cfgPath = Join-Path $ReleaseRoot "cmp-server.cfg" + if (Test-Path -LiteralPath $cfgPath) { + Get-Content -LiteralPath $cfgPath -ErrorAction SilentlyContinue | ForEach-Object { + $line = $_.Trim() + if ([string]::IsNullOrWhiteSpace($line) -or $line.StartsWith("#")) { return } + $parts = $line.Split("=", 2) + if ($parts.Count -ne 2) { return } + $key = $parts[0].Trim().ToLowerInvariant() + $value = $parts[1].Trim() + switch ($key) { + "host" { if ($value) { $hostName = $value } } + "port" { + $parsed = 0 + if ([int]::TryParse($value, [ref]$parsed) -and $parsed -gt 0 -and $parsed -le 65535) { + $port = $parsed + } + } + } + } + } + + return [pscustomobject]@{ + Host = $hostName + Port = $port + # Public CMP v0.1 parses argv as: --online --ip HOST --port PORT + # (exact tokens; --ip=HOST is ignored and it falls back to 127.0.0.1:11778) + LaunchOptions = "--online --ip $hostName --port $port" + } +} + +function Find-CyberpunkMpClientDll { + param([string]$GameRoot) + + $candidates = @( + (Join-Path $GameRoot "red4ext\plugins\zzzCyberpunkMP\CyberpunkMP.dll"), + (Join-Path $GameRoot "red4ext\plugins\CyberpunkMP\CyberpunkMP.dll"), + (Join-Path $GameRoot "red4ext\plugins\CyberpunkMP.dll") + ) + + foreach ($path in $candidates) { + if (Test-Path -LiteralPath $path) { + return $path + } + } + + return "" +} + +function Get-SteamRoot { + foreach ($regPath in @("HKCU:\Software\Valve\Steam", "HKLM:\SOFTWARE\WOW6432Node\Valve\Steam")) { + $props = Get-ItemProperty -Path $regPath -ErrorAction SilentlyContinue + if ($props) { + foreach ($name in @("SteamPath", "InstallPath")) { + if ($props.PSObject.Properties[$name]) { + $candidate = ($props.$name).Replace('/', '\') + if (Test-Path -LiteralPath (Join-Path $candidate "steam.exe")) { + return $candidate + } + } + } + } + } + return "" +} + +function Stop-SteamIfRunning([string]$SteamRoot) { + $steam = Get-Process -Name "steam" -ErrorAction SilentlyContinue + if (-not $steam) { return $false } + + Write-Host "Closing Steam so launch options can be saved..." + Start-Process -FilePath (Join-Path $SteamRoot "steam.exe") -ArgumentList "-shutdown" + + $deadline = (Get-Date).AddSeconds(30) + while ((Get-Date) -lt $deadline -and (Get-Process -Name "steam" -ErrorAction SilentlyContinue)) { + Start-Sleep -Seconds 1 + } + + $still = Get-Process -Name "steam" -ErrorAction SilentlyContinue + if ($still) { + Write-Warning "Steam did not close in time; killing the process." + $still | Stop-Process -Force + Start-Sleep -Seconds 2 + } + + return $true +} + +function Set-LaunchOptionsInVdf { + param( + [string]$VdfPath, + [string]$AppId, + [string]$LaunchOptions + ) + + $content = [System.IO.File]::ReadAllText($VdfPath) + if ([string]::IsNullOrWhiteSpace($content)) { return $false } + + $appKeyPattern = "(?m)^\s*""$AppId""\s*$" + $appMatch = [regex]::Match($content, $appKeyPattern) + if (-not $appMatch.Success) { + return $false + } + + $braceStart = $content.IndexOf('{', $appMatch.Index) + if ($braceStart -lt 0) { return $false } + + $depth = 0 + $braceEnd = -1 + for ($i = $braceStart; $i -lt $content.Length; $i++) { + switch ($content[$i]) { + '{' { $depth++ } + '}' { + $depth-- + if ($depth -eq 0) { $braceEnd = $i; break } + } + } + if ($braceEnd -ge 0) { break } + } + if ($braceEnd -lt 0) { return $false } + + $blockBody = $content.Substring($braceStart, $braceEnd - $braceStart + 1) + + $escaped = $LaunchOptions.Replace('\', '\\').Replace('"', '\"') + $launchLine = "`"LaunchOptions`"`t`t`"$escaped`"" + + $optionsPattern = '(?m)^\s*"LaunchOptions"\s+".*?"\s*$' + if ([regex]::IsMatch($blockBody, $optionsPattern)) { + $newBlock = [regex]::Replace($blockBody, $optionsPattern, "`t`t`t`t`t$launchLine", 1) + } else { + $newBlock = $blockBody.Insert(1, "`n`t`t`t`t`t$launchLine") + } + + $newContent = $content.Substring(0, $braceStart) + $newBlock + $content.Substring($braceEnd + 1) + + Copy-Item -LiteralPath $VdfPath "$VdfPath.cmp-backup" -Force + [System.IO.File]::WriteAllText($VdfPath, $newContent) + return $true +} + +function Set-SteamLaunchOptions { + param([string]$LaunchOptions) + + $CyberpunkAppId = "1091500" + $steamRoot = Get-SteamRoot + if ([string]::IsNullOrWhiteSpace($steamRoot)) { + Write-Warning "Steam installation not found; skipping launch-option update." + Write-Host "Use 'CyberpunkMP - Play.bat' instead." + return + } + + $userdata = Join-Path $steamRoot "userdata" + if (-not (Test-Path -LiteralPath $userdata)) { + Write-Warning "No Steam userdata folder found; skipping launch-option update." + return + } + + $configs = Get-ChildItem -LiteralPath $userdata -Directory | + ForEach-Object { Join-Path $_.FullName "config\localconfig.vdf" } | + Where-Object { Test-Path -LiteralPath $_ } + + if (-not $configs) { + Write-Warning "No Steam profiles found; skipping launch-option update." + return + } + + $steamWasRunning = Stop-SteamIfRunning $steamRoot + + $updated = 0 + foreach ($config in $configs) { + if (Set-LaunchOptionsInVdf -VdfPath $config -AppId $CyberpunkAppId -LaunchOptions $LaunchOptions) { + $updated++ + } + } + + if ($updated -gt 0) { + Write-Host "[ok] Steam launch options set for $updated profile(s):" + Write-Host " $LaunchOptions" + Write-Host " (backup saved next to each localconfig.vdf as *.cmp-backup)" + } else { + Write-Warning "Cyberpunk 2077 was not found in any Steam profile." + Write-Warning "Launch the game once from Steam, then re-run this script." + Write-Host "Until then, use 'CyberpunkMP - Play.bat'." + } + + if ($steamWasRunning) { + Write-Host "Restarting Steam..." + Start-Process -FilePath (Join-Path $steamRoot "steam.exe") + } +} diff --git a/release/CmpRelay.ps1 b/release/CmpRelay.ps1 new file mode 100644 index 00000000..a191f83e --- /dev/null +++ b/release/CmpRelay.ps1 @@ -0,0 +1,60 @@ +#Requires -Version 5.1 +# UDP relay: CyberpunkMP default 127.0.0.1:11778 -> cloud playit endpoint +param( + [string]$RemoteHost = "147.185.221.225", + [int]$RemotePort = 29309, + [int]$ListenPort = 11778 +) + +$ErrorActionPreference = "Continue" +Write-Host "========================================" +Write-Host " CMP RELAY 127.0.0.1:$ListenPort" +Write-Host " -> ${RemoteHost}:$RemotePort" +Write-Host " Keep this window OPEN while you play" +Write-Host "========================================" + +$local = New-Object System.Net.Sockets.UdpClient +$local.Client.SetSocketOption([System.Net.Sockets.SocketOptionLevel]::Socket, [System.Net.Sockets.SocketOptionName]::ReuseAddress, $true) +$local.Client.Bind((New-Object System.Net.IPEndPoint ([System.Net.IPAddress]::Loopback, $ListenPort))) +$local.Client.ReceiveTimeout = 50 + +$remote = New-Object System.Net.Sockets.UdpClient +$remote.Client.ReceiveTimeout = 50 +$remoteEP = New-Object System.Net.IPEndPoint ([System.Net.IPAddress]::Parse($RemoteHost), $RemotePort) + +$gameEP = $null +$fwd = 0 +$back = 0 + +while ($true) { + try { + $ep = New-Object System.Net.IPEndPoint ([System.Net.IPAddress]::Any, 0) + $data = $local.Receive([ref]$ep) + $gameEP = New-Object System.Net.IPEndPoint $ep.Address, $ep.Port + [void]$remote.Send($data, $data.Length, $remoteEP) + $fwd++ + if ($fwd -eq 1 -or ($fwd % 50) -eq 0) { + Write-Host ("[{0}] game->cloud packets={1} bytes={2}" -f (Get-Date -Format "HH:mm:ss"), $fwd, $data.Length) + } + } catch [System.Net.Sockets.SocketException] { + # timeout ok + } catch { + Write-Host "local recv: $_" + } + + if ($null -ne $gameEP) { + try { + $ep2 = New-Object System.Net.IPEndPoint ([System.Net.IPAddress]::Any, 0) + $data2 = $remote.Receive([ref]$ep2) + [void]$local.Send($data2, $data2.Length, $gameEP) + $back++ + if ($back -eq 1 -or ($back % 50) -eq 0) { + Write-Host ("[{0}] cloud->game packets={1} bytes={2}" -f (Get-Date -Format "HH:mm:ss"), $back, $data2.Length) + } + } catch [System.Net.Sockets.SocketException] { + # timeout ok + } catch { + Write-Host "remote recv: $_" + } + } +} diff --git a/release/CyberpunkMP - Play.bat b/release/CyberpunkMP - Play.bat new file mode 100644 index 00000000..a809b224 --- /dev/null +++ b/release/CyberpunkMP - Play.bat @@ -0,0 +1,8 @@ +@echo off +cd /d "C:\Program Files (x86)\Steam\steamapps\common\Cyberpunk 2077\bin\x64" +echo Launching with: +echo --online --ip sites-avon.gl.at.ply.gg --port 29309 +echo. +start "" "Cyberpunk2077.exe" --online --ip sites-avon.gl.at.ply.gg --port 29309 +echo Game started. Load into Night City, then HOLD / for 1 second. +pause diff --git a/release/Disable-RedSync-Client.bat b/release/Disable-RedSync-Client.bat new file mode 100644 index 00000000..84117f3e --- /dev/null +++ b/release/Disable-RedSync-Client.bat @@ -0,0 +1,3 @@ +@echo off +powershell -NoProfile -ExecutionPolicy Bypass -File "%~dp0Disable-RedSync-Client.ps1" -WaitForGameClose +if errorlevel 1 pause diff --git a/release/Disable-RedSync-Client.ps1 b/release/Disable-RedSync-Client.ps1 new file mode 100644 index 00000000..c1cbfc46 --- /dev/null +++ b/release/Disable-RedSync-Client.ps1 @@ -0,0 +1,65 @@ +#Requires -Version 5.1 +<# +.SYNOPSIS + Remove the legacy RedSync RED4ext/redscript client so the old "use Play.bat" HUD stops. +#> +param( + [string]$GameDir = "", + [switch]$WaitForGameClose +) + +$ErrorActionPreference = "Stop" +$ReleaseRoot = Split-Path -Parent $MyInvocation.MyCommand.Path +. (Join-Path $ReleaseRoot "GamePath.ps1") + +try { + Assert-GameNotRunning -Wait:$WaitForGameClose + $GameDir = Resolve-GameDir -Requested $GameDir -ReleaseRoot $ReleaseRoot -AllowPrompt +} catch { + Write-Host $_.Exception.Message + Read-Host "Press Enter to exit" + exit 1 +} + +Write-Host "Removing legacy RedSync client from: $GameDir" + +$paths = @( + (Join-Path $GameDir "red4ext\plugins\RedSync"), + (Join-Path $GameDir "red4ext\plugins\RedSync.Red4Ext.dll"), + (Join-Path $GameDir "r6\scripts\RedSync") +) + +$removed = $false +foreach ($path in $paths) { + if (Test-Path -LiteralPath $path) { + Remove-Item -LiteralPath $path -Recurse -Force -ErrorAction SilentlyContinue + Write-Host "Removed: $path" + $removed = $true + } +} + +foreach ($root in @((Join-Path $GameDir "red4ext\plugins"), (Join-Path $GameDir "r6\scripts"))) { + if (Test-Path -LiteralPath $root) { + Get-ChildItem -LiteralPath $root -Force -ErrorAction SilentlyContinue | + Where-Object { $_.Name -like "RedSync*" } | + ForEach-Object { + Remove-Item -LiteralPath $_.FullName -Recurse -Force -ErrorAction SilentlyContinue + Write-Host "Removed: $($_.FullName)" + $removed = $true + } + } +} + +$cacheFile = Join-Path $GameDir "r6\cache\final.redscripts.modded" +if (Test-Path -LiteralPath $cacheFile) { + Remove-Item -LiteralPath $cacheFile -Force -ErrorAction SilentlyContinue + Write-Host "Cleared redscript cache." +} + +Write-Host "" +if ($removed) { + Write-Host "Legacy RedSync client removed. Use CyberpunkMP - Play.bat / Setup-CyberpunkMP.bat next." +} else { + Write-Host "No RedSync client files found." +} +Read-Host "Press Enter to exit" diff --git a/release/Fix-Connect-F6.ps1 b/release/Fix-Connect-F6.ps1 new file mode 100644 index 00000000..db75f047 --- /dev/null +++ b/release/Fix-Connect-F6.ps1 @@ -0,0 +1,190 @@ +#Requires -Version 5.1 +$ErrorActionPreference = "Stop" + +$game = "C:\Program Files (x86)\Steam\steamapps\common\Cyberpunk 2077" +$inputXml = Join-Path $game "r6\input\CyberpunkMP.xml" +$pluginDll = Join-Path $game "red4ext\plugins\zzzCyberpunkMP\CyberpunkMP.dll" +$pluginLog = Join-Path $game "red4ext\plugins\zzzCyberpunkMP\CyberpunkMP.log" +$playBat = Join-Path $env:USERPROFILE "Downloads\RedSync-CMP-Connect\PLAY.bat" +if (-not (Test-Path $playBat)) { + $playBat = Join-Path $env:USERPROFILE "Downloads\RedSync-CMP-Connect\release\PLAY.bat" +} +if (-not (Test-Path $playBat)) { + $playBat = Join-Path $env:USERPROFILE "Desktop\PLAY.bat" +} + +Write-Host "Game: $game" + +if (-not (Test-Path $pluginDll)) { + Write-Host "[FAIL] CyberpunkMP.dll missing at $pluginDll" + exit 1 +} +Write-Host "[ok] CyberpunkMP.dll present" + +$inputLoader = Join-Path $game "red4ext\plugins\input_loader\input_loader.dll" +if (-not (Test-Path $inputLoader)) { + Write-Host "[FAIL] Input Loader missing. Re-run Install-CyberpunkMP-Client.bat" + exit 1 +} +Write-Host "[ok] Input Loader present" + +New-Item -ItemType Directory -Force -Path (Split-Path $inputXml) | Out-Null + +# Prefer F6 for connect/disconnect - Slash fails on many layouts +@' + + + + + + + + + + + + +