C#:
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Game.Rust.Cui;
using UnityEngine;
using UnityEngine.UI;
namespace Oxide.Plugins
{
[Info("DeepSeaAdmin", "jerky/claude", "1.1.0")]
[Description("DeepSea管理パネル - サーバー管理者向けDeepSea設定UI")]
class DeepSeaAdmin : RustPlugin
{
const string PERM = "deepseaadmin.use";
const string UI = "DSM_UI";
const string SCROLL = "DSM_SC";
int _row;
ulong _currentBuildPlayer;
// Per-player y-offsets of bool rows (for selective update without full re-render)
readonly Dictionary<ulong, Dictionary<string, float>> _boolY =
new Dictionary<ulong, Dictionary<string, float>>();
// ─── Config ───────────────────────────────────────────────────────────
class Cfg
{
// Basic
public bool allVehicles = false, swimmers = false,
blockBuilding = true, logs = false, openOnWipe = false,
debugPortal = false, npcSpawnOnIslands = true,
fogOfWar = true;
// Timing
public float wipeDuration = 10800, cooldownMin = 5400, cooldownMax = 5400,
wipeEndPhase = 1800, radPhase = 300,
entityInterval = 5, groupInterval = 0.15f;
// Islands
public int islandCount = 6;
public float islandRadius = 300, islandEdge = 750, islandMinDist = 600,
islandPortalDist = 750;
// Floating City
public int cityCount = 1;
public float cityRadius = 500, cityEdge = 1500, cityMinDist = 1500;
// Ghost Ships
public int ghostCount = 4;
public float ghostRadius = 80, ghostEdge = 450, ghostMinDist = 400;
// RHIB
public int rhibCount = 4;
public float rhibRadius = 10, rhibEdge = 1150, rhibMinDist = 300;
// Misc
public int crateCount = 1, portalDir = 0;
public Cfg Clone() => JsonConvert.DeserializeObject<Cfg>(JsonConvert.SerializeObject(this));
}
Cfg cfg;
bool _firstRun;
// Per-player pending config (discarded on close without Apply)
readonly Dictionary<ulong, Cfg> _pending = new Dictionary<ulong, Cfg>();
protected override void LoadConfig()
{
base.LoadConfig();
try { cfg = Config.ReadObject<Cfg>(); if (cfg == null) cfg = new Cfg(); }
catch { cfg = new Cfg(); }
}
protected override void SaveConfig() => Config.WriteObject(cfg);
protected override void LoadDefaultConfig() { cfg = new Cfg(); _firstRun = true; }
// ─── Lifecycle ────────────────────────────────────────────────────────
void Init() => permission.RegisterPermission(PERM, this);
void OnServerInitialized()
{
if (_firstRun)
{
cfg = ReadFromServer();
SaveConfig();
_firstRun = false;
Puts("First run: saved server values to config.");
}
else
{
ApplyAll(cfg);
}
}
// ─── Read current server values into Cfg ──────────────────────────────
Cfg ReadFromServer()
{
string V(string name)
{
var cmd = ConsoleSystem.Index.Server.Find(name);
return cmd?.String ?? "";
}
bool B(string name) => !V(name).Equals("False", StringComparison.OrdinalIgnoreCase);
float F(string name) { float v; return float.TryParse(V(name), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out v) ? v : 0f; }
int I(string name) { int v; return int.TryParse(V(name), out v) ? v : 0; }
return new Cfg
{
allVehicles = B("deepsea.allow_all_vehicles"),
swimmers = B("deepsea.allow_swimmers"),
blockBuilding = B("deepsea.block_building"),
logs = B("deepsea.logs"),
openOnWipe = B("deepsea.openonserverwipe"),
debugPortal = B("deepsea.debug_portal_spawnattempts"),
npcSpawnOnIslands = B("ai.npc_spawn_on_deep_sea_islands"),
fogOfWar = B("server.deepseafogofwar"),
wipeDuration = F("deepsea.wipeduration"),
cooldownMin = F("deepsea.wipecooldownmin"),
cooldownMax = F("deepsea.wipecooldownmax"),
wipeEndPhase = F("deepsea.wipeendphaseduration"),
radPhase = F("deepsea.wiperadiationphaseduration"),
entityInterval = F("deepsea.entities_spawninterval"),
groupInterval = F("deepsea.spawngroups_spawninterval"),
islandCount = I("deepsea.island_count"),
islandRadius = F("deepsea.island_radius"),
islandEdge = F("deepsea.island_edgemargin"),
islandMinDist = F("deepsea.island_mindist"),
islandPortalDist = F("deepsea.island_portal_terrain_distance"),
cityCount = I("deepsea.floatingcity_count"),
cityRadius = F("deepsea.floatingcity_radius"),
cityEdge = F("deepsea.floatingcity_edgemargin"),
cityMinDist = F("deepsea.floatingcity_mindist"),
ghostCount = I("deepsea.ghostship_count"),
ghostRadius = F("deepsea.ghostship_radius"),
ghostEdge = F("deepsea.ghostship_edgemargin"),
ghostMinDist = F("deepsea.ghostship_mindist"),
rhibCount = I("deepsea.rhib_count"),
rhibRadius = F("deepsea.rhib_radius"),
rhibEdge = F("deepsea.rhib_edgemargin"),
rhibMinDist = F("deepsea.rhib_mindist"),
crateCount = I("deepsea.hackablecrate_count"),
portalDir = I("deepsea.forceentranceportaldirection"),
};
}
void Unload()
{
foreach (var p in BasePlayer.activePlayerList)
CuiHelper.DestroyUi(p, UI);
}
bool HasPerm(BasePlayer p) => p.IsAdmin || permission.UserHasPermission(p.UserIDString, PERM);
Cfg PendingOf(BasePlayer p)
{
if (!_pending.TryGetValue(p.userID, out var c)) { c = cfg.Clone(); _pending[p.userID] = c; }
return c;
}
// ─── Apply all settings to server ────────────────────────────────────
void ApplyAll(Cfg c)
{
SRun($"deepsea.allow_all_vehicles {B(c.allVehicles)}");
SRun($"deepsea.allow_swimmers {B(c.swimmers)}");
SRun($"deepsea.block_building {B(c.blockBuilding)}");
SRun($"deepsea.logs {B(c.logs)}");
SRun($"deepsea.openonserverwipe {B(c.openOnWipe)}");
SRun($"deepsea.debug_portal_spawnattempts {B(c.debugPortal)}");
SRun($"ai.npc_spawn_on_deep_sea_islands {B(c.npcSpawnOnIslands)}");
SRun($"server.deepseafogofwar {B(c.fogOfWar)}");
SRun($"deepsea.wipeduration {c.wipeDuration}");
SRun($"deepsea.wipecooldownmin {c.cooldownMin}");
SRun($"deepsea.wipecooldownmax {c.cooldownMax}");
SRun($"deepsea.wipeendphaseduration {c.wipeEndPhase}");
SRun($"deepsea.wiperadiationphaseduration {c.radPhase}");
SRun($"deepsea.entities_spawninterval {c.entityInterval}");
SRun($"deepsea.spawngroups_spawninterval {c.groupInterval}");
SRun($"deepsea.island_count {c.islandCount}");
SRun($"deepsea.island_radius {c.islandRadius}");
SRun($"deepsea.island_edgemargin {c.islandEdge}");
SRun($"deepsea.island_mindist {c.islandMinDist}");
SRun($"deepsea.island_portal_terrain_distance {c.islandPortalDist}");
SRun($"deepsea.floatingcity_count {c.cityCount}");
SRun($"deepsea.floatingcity_radius {c.cityRadius}");
SRun($"deepsea.floatingcity_edgemargin {c.cityEdge}");
SRun($"deepsea.floatingcity_mindist {c.cityMinDist}");
SRun($"deepsea.ghostship_count {c.ghostCount}");
SRun($"deepsea.ghostship_radius {c.ghostRadius}");
SRun($"deepsea.ghostship_edgemargin {c.ghostEdge}");
SRun($"deepsea.ghostship_mindist {c.ghostMinDist}");
SRun($"deepsea.rhib_count {c.rhibCount}");
SRun($"deepsea.rhib_radius {c.rhibRadius}");
SRun($"deepsea.rhib_edgemargin {c.rhibEdge}");
SRun($"deepsea.rhib_mindist {c.rhibMinDist}");
SRun($"deepsea.hackablecrate_count {c.crateCount}");
SRun($"deepsea.forceentranceportaldirection {c.portalDir}");
}
string B(bool v) => v ? "true" : "false";
void SRun(string cmd) => ConsoleSystem.Run(ConsoleSystem.Option.Server.Quiet(), cmd);
string SGet(string cmd) => ConsoleSystem.Run(ConsoleSystem.Option.Server.Quiet(), cmd) ?? "";
// ─── Chat Commands ────────────────────────────────────────────────────
[ChatCommand("dsm")]
void CmdDsm(BasePlayer p, string _, string[] args)
{
if (!HasPerm(p)) { Chat(p, "権限がありません。"); return; }
if (_firstRun)
{
SaveConfig();
_firstRun = false;
Puts("First run: saved default config. Server settings unchanged.");
}
if (SGet("deepsea.enabled").Equals("False", StringComparison.OrdinalIgnoreCase))
{ Chat(p, "DeepSeaが無効です。(deepsea.enabled = false)"); return; }
OpenUI(p);
}
// ─── Console Commands ─────────────────────────────────────────────────
[ConsoleCommand("dsm.close")]
void CmdClose(ConsoleSystem.Arg a)
{
var p = a.Player();
if (p == null) return;
_pending.Remove(p.userID); // discard pending → revert
CuiHelper.DestroyUi(p, UI);
}
[ConsoleCommand("dsm.toggle")]
void CmdToggle(ConsoleSystem.Arg a)
{
var p = a.Player();
if (p == null || !HasPerm(p)) return;
var c = PendingOf(p);
string field = a.GetString(0);
switch (field)
{
case "allvehicles": c.allVehicles = !c.allVehicles; break;
case "swimmers": c.swimmers = !c.swimmers; break;
case "blockbuilding": c.blockBuilding = !c.blockBuilding; break;
case "logs": c.logs = !c.logs; break;
case "openwipe": c.openOnWipe = !c.openOnWipe; break;
case "debugportal": c.debugPortal = !c.debugPortal; break;
case "npcspawn": c.npcSpawnOnIslands = !c.npcSpawnOnIslands;break;
case "fogofwar": c.fogOfWar = !c.fogOfWar; break;
}
bool newVal = BoolFromField(field, c);
bool interactive = !SGet("deepsea.enabled").Equals("False", StringComparison.OrdinalIgnoreCase);
if (interactive &&
_boolY.TryGetValue(p.userID, out var yMap) &&
yMap.TryGetValue(field, out float y))
{
CuiHelper.DestroyUi(p, $"DSM_CB_{field}");
var upd = new CuiElementContainer();
string cc = newVal ? "0.25 0.58 0.18 1.0" : "0.26 0.26 0.24 1.0";
upd.Add(new CuiButton
{
Button = { Command = $"dsm.toggle {field}", Color = cc },
RectTransform = { AnchorMin = "0.70 1", AnchorMax = "1 1",
OffsetMin = $"4 {-(y+RH-5)}", OffsetMax = $"-6 {-(y+5)}" },
Text = { Text = newVal ? "✓ ON" : "✗ OFF", FontSize = 13,
Align = TextAnchor.MiddleCenter, Color = "1 1 1 1" }
}, SCROLL, $"DSM_CB_{field}");
CuiHelper.AddUi(p, upd);
}
else
{
OpenUI(p); // fallback
}
}
bool BoolFromField(string field, Cfg c)
{
return field switch
{
"allvehicles" => c.allVehicles,
"swimmers" => c.swimmers,
"blockbuilding" => c.blockBuilding,
"logs" => c.logs,
"openwipe" => c.openOnWipe,
"debugportal" => c.debugPortal,
"npcspawn" => c.npcSpawnOnIslands,
"fogofwar" => c.fogOfWar,
_ => false
};
}
[ConsoleCommand("dsm.set")]
void CmdSet(ConsoleSystem.Arg a)
{
var p = a.Player();
if (p == null || !HasPerm(p)) return;
var c = PendingOf(p);
if (SGet("deepsea.enabled").Equals("False", StringComparison.OrdinalIgnoreCase)) return;
string f = a.GetString(0), v = a.GetString(1);
float fv; int iv;
switch (f)
{
case "wipeduration": if (float.TryParse(v, out fv)) c.wipeDuration = fv; break;
case "cooldownmin": if (float.TryParse(v, out fv)) c.cooldownMin = fv; break;
case "cooldownmax": if (float.TryParse(v, out fv)) c.cooldownMax = fv; break;
case "wipeendphase": if (float.TryParse(v, out fv)) c.wipeEndPhase = fv; break;
case "radphase": if (float.TryParse(v, out fv)) c.radPhase = fv; break;
case "entityinterval": if (float.TryParse(v, out fv)) c.entityInterval = fv; break;
case "groupinterval": if (float.TryParse(v, out fv)) c.groupInterval = fv; break;
case "islandcount": if (int.TryParse(v, out iv)) c.islandCount = iv; break;
case "islandradius": if (float.TryParse(v, out fv)) c.islandRadius = fv; break;
case "islandedge": if (float.TryParse(v, out fv)) c.islandEdge = fv; break;
case "islandmindist": if (float.TryParse(v, out fv)) c.islandMinDist = fv; break;
case "islandpdist": if (float.TryParse(v, out fv)) c.islandPortalDist= fv; break;
case "citycount": if (int.TryParse(v, out iv)) c.cityCount = iv; break;
case "cityradius": if (float.TryParse(v, out fv)) c.cityRadius = fv; break;
case "cityedge": if (float.TryParse(v, out fv)) c.cityEdge = fv; break;
case "citymindist": if (float.TryParse(v, out fv)) c.cityMinDist = fv; break;
case "ghostcount": if (int.TryParse(v, out iv)) c.ghostCount = iv; break;
case "ghostradius": if (float.TryParse(v, out fv)) c.ghostRadius = fv; break;
case "ghostedge": if (float.TryParse(v, out fv)) c.ghostEdge = fv; break;
case "ghostmindist": if (float.TryParse(v, out fv)) c.ghostMinDist = fv; break;
case "rhibcount": if (int.TryParse(v, out iv)) c.rhibCount = iv; break;
case "rhibradius": if (float.TryParse(v, out fv)) c.rhibRadius = fv; break;
case "rhibedge": if (float.TryParse(v, out fv)) c.rhibEdge = fv; break;
case "rhibmindist": if (float.TryParse(v, out fv)) c.rhibMinDist = fv; break;
case "cratecount": if (int.TryParse(v, out iv)) c.crateCount = iv; break;
case "portaldir": if (int.TryParse(v, out iv)) c.portalDir = iv; break;
}
// No UI refresh on text input (cursor would reset)
}
[ConsoleCommand("dsm.apply")]
void CmdApply(ConsoleSystem.Arg a)
{
var p = a.Player();
if (p == null || !HasPerm(p)) return;
cfg = PendingOf(p).Clone();
SaveConfig();
ApplyAll(cfg);
_pending[p.userID] = cfg.Clone(); // sync pending with saved
Chat(p, "設定を反映しました。");
OpenUI(p);
}
[ConsoleCommand("dsm.action")]
void CmdAction(ConsoleSystem.Arg a)
{
var p = a.Player();
if (p == null || !HasPerm(p)) return;
string act = a.GetString(0);
switch (act)
{
case "open":
SRun("deepsea.open");
Chat(p, "DeepSeaの強制オープンを実行しました。オープン完了までしばらくお待ちください。状況はステータスで確認できます。");
break;
case "close":
_pending.Remove(p.userID);
CuiHelper.DestroyUi(p, UI);
Chat(p, "<color=#f5c842>全プレイヤーをDeepSeaから退出させています...</color>");
WaitAndClose(p);
break;
case "spawnisland":
case "spawnghostship":
case "spawncity":
case "spawnrhib":
SpawnAtPlayer(p, act);
break;
case "status":
SendStatusToChat(p, "deepsea.status", "DeepSea ステータス");
break;
case "entitycount":
SendStatusToChat(p, "deepsea.printentitycount", "エンティティ数");
break;
case "loot":
SendStatusToChat(p, "deepsea.printloot", "戦利品情報");
break;
case "timetowipe":
SendStatusToChat(p, "deepsea.printtimetowipe", "ワイプまでの残り時間");
break;
case "enterdeepsea":
if (!p.IsAdmin) { Chat(p, "この操作にはサーバー管理者権限が必要です。"); return; }
p.SendConsoleCommand("deepsea.enterdeepsea");
Chat(p, "DeepSeaへ移動します...");
break;
case "leavedeepsea":
if (!p.IsAdmin) { Chat(p, "この操作にはサーバー管理者権限が必要です。"); return; }
p.SendConsoleCommand("deepsea.leavedeepsea");
Chat(p, "DeepSeaから出ます...");
break;
}
}
void SendStatusToChat(BasePlayer p, string cmd, string title)
{
string result = SGet(cmd);
if (string.IsNullOrEmpty(result))
result = "(データなし。DeepSeaが起動していない可能性があります)";
Chat(p, $"<color=#f5c842>── {title} ──</color>\n{result}");
}
void SpawnAtPlayer(BasePlayer p, string act)
{
if (!p.IsAdmin)
{
Chat(p, "スポーン操作にはサーバー管理者権限が必要です。");
return;
}
string cmd = null;
string name = "";
switch (act)
{
case "spawnisland": cmd = "deepsea.spawnislandhere"; name = "島"; break;
case "spawnghostship": cmd = "deepsea.spawnghostshiphere"; name = "幽霊船"; break;
case "spawncity": cmd = "deepsea.spawnfloatingcityhere"; name = "浮遊都市"; break;
case "spawnrhib": cmd = "deepsea.spawnrhibgrouphere"; name = "RHIBグループ"; break;
}
if (cmd == null) return;
Chat(p, $"現在地に{name}を生成しています...");
p.SendConsoleCommand(cmd);
timer.Once(2f, () => { if (p != null && p.IsConnected) Chat(p, $"<color=#7fc87f>{name}の生成が完了しました。</color>"); });
}
void Chat(BasePlayer p, string msg) => p.ChatMessage($"<color=#c8a96e>[DSM]</color> {msg}");
void WaitAndClose(BasePlayer initiator)
{
foreach (var bp in BasePlayer.activePlayerList)
bp.SendConsoleCommand("deepsea.leavedeepsea");
timer.Once(5f, () =>
{
SRun("deepsea.close");
if (initiator != null && initiator.IsConnected)
Chat(initiator, "DeepSeaを強制クローズしました。");
});
}
// ─── UI ───────────────────────────────────────────────────────────────
const float RH = 38f;
const float SH = 33f;
// DeepSeaが稼働中か判定(status出力の "Open: True" で確認)
bool IsDeepSeaActive()
{
string s = SGet("deepsea.status");
if (string.IsNullOrEmpty(s)) return false;
return s.Contains("Open: True");
}
void OpenUI(BasePlayer player)
{
CuiHelper.DestroyUi(player, UI);
_row = 0;
_currentBuildPlayer = player.userID;
var cur = PendingOf(player);
bool en = !SGet("deepsea.enabled").Equals("False", StringComparison.OrdinalIgnoreCase);
bool active = IsDeepSeaActive();
var c = new CuiElementContainer();
// Outer panel — position managed client-side by CuiDraggableComponent
c.Add(new CuiElement
{
Name = UI, Parent = "Overlay",
Components =
{
new CuiImageComponent { Color ="0.08 0.08 0.07 0.97" },
new CuiRectTransformComponent { AnchorMin = "0.15 0.12", AnchorMax = "0.85 0.88" },
new CuiNeedsCursorComponent(),
new CuiDraggableComponent()
}
});
// Title bar
c.Add(new CuiElement { Parent = UI, Components = {
new CuiImageComponent { Color ="0.18 0.15 0.10 1.0" },
new CuiRectTransformComponent { AnchorMin = "0 1", AnchorMax = "1 1", OffsetMin = "0 -44", OffsetMax = "0 0" }
}});
c.Add(new CuiLabel { Text = { Text = "Deep Sea Manager", FontSize = 17, Align = TextAnchor.MiddleCenter, Color = "0.95 0.85 0.58 1.0" },
RectTransform = { AnchorMin = "0 1", AnchorMax = "0.92 1", OffsetMin = "0 -44", OffsetMax = "0 0" }}, UI);
c.Add(new CuiButton { Button = { Command = "dsm.close", Color = "0.58 0.15 0.10 0.9" },
RectTransform = { AnchorMin = "1 1", AnchorMax = "1 1", OffsetMin = "-48 -40", OffsetMax = "-4 -4" },
Text = { Text = "✕", FontSize = 16, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1" }}, UI);
// Status bar (below title, 36px)
string statusBgColor = active ? "0.10 0.22 0.10 1.0" : "0.22 0.10 0.08 1.0";
string statusDotColor = active ? "0.30 0.80 0.30 1.0" : "0.85 0.28 0.20 1.0";
string statusText = active ? "● 稼働中" : "● 停止中";
c.Add(new CuiElement { Parent = UI, Components = {
new CuiImageComponent { Color =statusBgColor },
new CuiRectTransformComponent { AnchorMin = "0 1", AnchorMax = "1 1", OffsetMin = "0 -80", OffsetMax = "0 -44" }
}});
c.Add(new CuiLabel { Text = { Text = statusText, FontSize = 13, Align = TextAnchor.MiddleLeft, Color = statusDotColor },
RectTransform = { AnchorMin = "0 1", AnchorMax = "0.35 1", OffsetMin = "12 -80", OffsetMax = "0 -44" }}, UI);
if (active)
{
// Enter / Leave ボタン(管理者のみ機能)
c.Add(new CuiButton { Button = { Command = "dsm.action enterdeepsea", Color = "0.20 0.38 0.52 1.0" },
RectTransform = { AnchorMin = "1 1", AnchorMax = "1 1", OffsetMin = "-310 -77", OffsetMax = "-160 -47" },
Text = { Text = "DeepSeaへ移動", FontSize = 11, Align = TextAnchor.MiddleCenter, Color = "0.95 0.92 0.88 1.0" }}, UI);
c.Add(new CuiButton { Button = { Command = "dsm.action leavedeepsea", Color = "0.42 0.28 0.18 1.0" },
RectTransform = { AnchorMin = "1 1", AnchorMax = "1 1", OffsetMin = "-155 -77", OffsetMax = "-5 -47" },
Text = { Text = "DeepSeaから出る", FontSize = 11, Align = TextAnchor.MiddleCenter, Color = "0.95 0.92 0.88 1.0" }}, UI);
}
// Bottom bar
c.Add(new CuiElement { Parent = UI, Components = {
new CuiImageComponent { Color ="0.12 0.11 0.09 1.0" },
new CuiRectTransformComponent { AnchorMin = "0 0", AnchorMax = "1 0", OffsetMin = "0 0", OffsetMax = "0 48" }
}});
c.Add(new CuiButton { Button = { Command = "dsm.apply", Color = "0.20 0.50 0.14 1.0" },
RectTransform = { AnchorMin = "0 0", AnchorMax = "0 0", OffsetMin = "8 7", OffsetMax = "210 41" },
Text = { Text = "▶ 反映 (Apply)", FontSize = 14, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1" }}, UI);
c.Add(new CuiLabel { Text = { Text = "※閉じると変更は破棄されます", FontSize = 10, Align = TextAnchor.MiddleLeft, Color = "0.65 0.62 0.55 1.0" },
RectTransform = { AnchorMin = "0 0", AnchorMax = "0.5 0", OffsetMin = "218 7", OffsetMax = "0 41" }}, UI);
c.Add(new CuiButton { Button = { Command = "dsm.close", Color = "0.30 0.28 0.25 1.0" },
RectTransform = { AnchorMin = "1 0", AnchorMax = "1 0", OffsetMin = "-148 7", OffsetMax = "-8 41" },
Text = { Text = "閉じる", FontSize = 14, Align = TextAnchor.MiddleCenter, Color = "0.80 0.78 0.74 1.0" }}, UI);
// Scroll view
c.Add(new CuiElement { Name = SCROLL, Parent = UI, Components = {
new CuiImageComponent { Color = "0.05 0.05 0.04 0.4" },
new CuiScrollViewComponent
{
ContentTransform = new CuiRectTransform { AnchorMin = "0 1", AnchorMax = "1 1", OffsetMin = "0 -2400", OffsetMax = "-10 0" },
Vertical = true, Horizontal = false,
MovementType = ScrollRect.MovementType.Elastic,
Elasticity = 0.1f, Inertia = true, DecelerationRate = 0.2f,
ScrollSensitivity = 35f,
VerticalScrollbar = new CuiScrollbar { Size = 8, AutoHide = false }
},
new CuiRectTransformComponent { AnchorMin = "0 0", AnchorMax = "1 1", OffsetMin = "0 48", OffsetMax = "0 -80" },
new CuiNeedsCursorComponent()
}});
// ── Content rows ──
float y = 0;
y = SecHead(c, "■ 基本設定 / Basic", y);
y = BoolRow(c, "全車両の通行許可", "allvehicles", cur.allVehicles, y, en);
y = BoolRow(c, "水泳での入場許可", "swimmers", cur.swimmers, y, en);
y = BoolRow(c, "建築禁止", "blockbuilding", cur.blockBuilding, y, en);
y = BoolRow(c, "ログ出力", "logs", cur.logs, y, en);
y = BoolRow(c, "ワイプ時に自動オープン", "openwipe", cur.openOnWipe, y, en);
y = BoolRow(c, "島へのNPCスポーン", "npcspawn", cur.npcSpawnOnIslands, y, en);
y = BoolRow(c, "Fog of War", "fogofwar", cur.fogOfWar, y, en);
y = BoolRow(c, "ポータルスポーンデバッグ","debugportal", cur.debugPortal, y, en);
y = SecHead(c, "■ 操作 / Actions", y);
y = ActionRow(c, en, y);
y = SecHead(c, "■ ステータス確認 / Status", y);
y = StatusRow(c, y);
y = SecHead(c, "■ タイミング / Timing", y);
y = FloatRow(c, "ワイプ継続時間 (秒)", "wipeduration", cur.wipeDuration, y, en);
y = FloatRow(c, "次回オープン最短待機 (秒)", "cooldownmin", cur.cooldownMin, y, en);
y = FloatRow(c, "次回オープン最長待機 (秒)", "cooldownmax", cur.cooldownMax, y, en);
y = FloatRow(c, "最終フェーズ時間 (秒)", "wipeendphase", cur.wipeEndPhase, y, en);
y = FloatRow(c, "放射線フェーズ開始前 (秒)", "radphase", cur.radPhase, y, en);
y = FloatRow(c, "エンティティスポーン間隔 (秒)","entityinterval", cur.entityInterval, y, en);
y = FloatRow(c, "スポーングループ間隔 (秒)", "groupinterval", cur.groupInterval, y, en);
y = SecHead(c, "■ 島の設定 / Islands", y);
y = IntRow (c, "島の数", "islandcount", cur.islandCount, y, en);
y = FloatRow(c, "島の半径", "islandradius", cur.islandRadius, y, en);
y = FloatRow(c, "島の端余白", "islandedge", cur.islandEdge, y, en);
y = FloatRow(c, "島の最小距離", "islandmindist",cur.islandMinDist, y, en);
y = FloatRow(c, "ポータルと地形の距離", "islandpdist", cur.islandPortalDist, y, en);
y = SecHead(c, "■ 浮遊都市 / Floating City", y);
y = IntRow (c, "浮遊都市の数", "citycount", cur.cityCount, y, en);
y = FloatRow(c, "浮遊都市の半径", "cityradius", cur.cityRadius, y, en);
y = FloatRow(c, "浮遊都市の端余白", "cityedge", cur.cityEdge, y, en);
y = FloatRow(c, "浮遊都市の最小距離","citymindist",cur.cityMinDist,y, en);
y = SecHead(c, "■ 幽霊船 / Ghost Ships", y);
y = IntRow (c, "幽霊船の数", "ghostcount", cur.ghostCount, y, en);
y = FloatRow(c, "幽霊船の半径", "ghostradius", cur.ghostRadius, y, en);
y = FloatRow(c, "幽霊船の端余白", "ghostedge", cur.ghostEdge, y, en);
y = FloatRow(c, "幽霊船の最小距離","ghostmindist",cur.ghostMinDist,y, en);
y = SecHead(c, "■ RHIB", y);
y = IntRow (c, "RHIBの数", "rhibcount", cur.rhibCount, y, en);
y = FloatRow(c, "RHIBの半径", "rhibradius", cur.rhibRadius, y, en);
y = FloatRow(c, "RHIBの端余白", "rhibedge", cur.rhibEdge, y, en);
y = FloatRow(c, "RHIBの最小距離","rhibmindist",cur.rhibMinDist,y, en);
y = SecHead(c, "■ その他 / Misc", y);
y = IntRow(c, "ハッカブルクレートの数", "cratecount", cur.crateCount, y, en);
y = IntRow(c, "入口ポータル方向 (0=ランダム 1=北 2=東 3=南 4=西)","portaldir", cur.portalDir, y, en);
CuiHelper.AddUi(player, c);
}
// ── UI helpers ────────────────────────────────────────────────────────
float SecHead(CuiElementContainer c, string title, float y)
{
c.Add(new CuiElement { Parent = SCROLL, Components = {
new CuiImageComponent { Color ="0.20 0.17 0.12 1.0" },
new CuiRectTransformComponent { AnchorMin = "0 1", AnchorMax = "1 1", OffsetMin = $"0 {-(y+SH)}", OffsetMax = $"0 {-y}" }
}});
c.Add(new CuiLabel { Text = { Text = title, FontSize = 12, Align = TextAnchor.MiddleLeft, Color = "0.92 0.74 0.40 1.0", Font = "RobotoCondensed-Bold.ttf" },
RectTransform = { AnchorMin = "0 1", AnchorMax = "1 1", OffsetMin = $"10 {-(y+SH)}", OffsetMax = $"0 {-y}" }}, SCROLL);
return y + SH + 1;
}
float WarnRow(CuiElementContainer c, string text, float y)
{
c.Add(new CuiLabel { Text = { Text = text, FontSize = 10, Align = TextAnchor.MiddleLeft, Color = "0.95 0.72 0.18 0.9" },
RectTransform = { AnchorMin = "0 1", AnchorMax = "1 1", OffsetMin = $"10 {-(y+24)}", OffsetMax = $"0 {-y}" }}, SCROLL);
return y + 24;
}
float BoolRow(CuiElementContainer c, string label, string field, bool val, float y, bool interactive)
{
string bg = (_row++ % 2 == 0) ? "0.13 0.13 0.11 1.0" : "0.16 0.16 0.14 1.0";
c.Add(new CuiElement { Parent = SCROLL, Components = {
new CuiImageComponent { Color =bg },
new CuiRectTransformComponent { AnchorMin = "0 1", AnchorMax = "1 1", OffsetMin = $"0 {-(y+RH)}", OffsetMax = $"0 {-y}" }
}});
string tc = interactive ? "0.88 0.85 0.80 1.0" : "0.44 0.43 0.41 1.0";
c.Add(new CuiLabel { Text = { Text = label, FontSize = 13, Align = TextAnchor.MiddleLeft, Color = tc },
RectTransform = { AnchorMin = "0 1", AnchorMax = "0.70 1", OffsetMin = $"10 {-(y+RH)}", OffsetMax = $"0 {-y}" }}, SCROLL);
if (interactive)
{
// Store y so dsm.toggle can selectively update just this button
if (_currentBuildPlayer != 0)
{
if (!_boolY.TryGetValue(_currentBuildPlayer, out var yMap))
_boolY[_currentBuildPlayer] = yMap = new Dictionary<string, float>();
yMap[field] = y;
}
string cc = val ? "0.25 0.58 0.18 1.0" : "0.26 0.26 0.24 1.0";
c.Add(new CuiButton { Button = { Command = $"dsm.toggle {field}", Color = cc },
RectTransform = { AnchorMin = "0.70 1", AnchorMax = "1 1", OffsetMin = $"4 {-(y+RH-5)}", OffsetMax = $"-6 {-(y+5)}" },
Text = { Text = val ? "✓ ON" : "✗ OFF", FontSize = 13, Align = TextAnchor.MiddleCenter, Color = "1 1 1 1" }}, SCROLL, $"DSM_CB_{field}");
}
else
{
c.Add(new CuiElement { Parent = SCROLL, Components = {
new CuiImageComponent { Color ="0.20 0.20 0.18 1.0" },
new CuiRectTransformComponent { AnchorMin = "0.70 1", AnchorMax = "1 1", OffsetMin = $"4 {-(y+RH-5)}", OffsetMax = $"-6 {-(y+5)}" }
}});
c.Add(new CuiLabel { Text = { Text = val ? "✓ ON" : "✗ OFF", FontSize = 13, Align = TextAnchor.MiddleCenter, Color = "0.38 0.38 0.36 1.0" },
RectTransform = { AnchorMin = "0.70 1", AnchorMax = "1 1", OffsetMin = $"4 {-(y+RH-5)}", OffsetMax = $"-6 {-(y+5)}" }}, SCROLL);
}
return y + RH + 1;
}
float FloatRow(CuiElementContainer c, string label, string field, float val, float y, bool en)
=> InputRow(c, label, field, val.ToString("G"), y, en);
float IntRow(CuiElementContainer c, string label, string field, int val, float y, bool en)
=> InputRow(c, label, field, val.ToString(), y, en);
float InputRow(CuiElementContainer c, string label, string field, string val, float y, bool en)
{
string bg = (_row++ % 2 == 0) ? "0.13 0.13 0.11 1.0" : "0.16 0.16 0.14 1.0";
c.Add(new CuiElement { Parent = SCROLL, Components = {
new CuiImageComponent { Color =bg },
new CuiRectTransformComponent { AnchorMin = "0 1", AnchorMax = "1 1", OffsetMin = $"0 {-(y+RH)}", OffsetMax = $"0 {-y}" }
}});
string tc = en ? "0.88 0.85 0.80 1.0" : "0.44 0.43 0.41 1.0";
c.Add(new CuiLabel { Text = { Text = label, FontSize = 12, Align = TextAnchor.MiddleLeft, Color = tc },
RectTransform = { AnchorMin = "0 1", AnchorMax = "0.60 1", OffsetMin = $"10 {-(y+RH)}", OffsetMax = $"0 {-y}" }}, SCROLL);
if (en)
{
c.Add(new CuiElement { Parent = SCROLL, Components = {
new CuiImageComponent { Color ="0.09 0.09 0.08 1.0" },
new CuiRectTransformComponent { AnchorMin = "0.61 1", AnchorMax = "1 1", OffsetMin = $"0 {-(y+RH-5)}", OffsetMax = $"-6 {-(y+5)}" }
}});
c.Add(new CuiElement { Parent = SCROLL, Components = {
new CuiInputFieldComponent { Text = val, FontSize = 13, Align = TextAnchor.MiddleCenter,
Color = "0.92 0.90 0.86 1.0", Command = $"dsm.set {field}", CharsLimit = 12, },
new CuiRectTransformComponent { AnchorMin = "0.61 1", AnchorMax = "1 1", OffsetMin = $"2 {-(y+RH-5)}", OffsetMax = $"-8 {-(y+5)}" }
}});
}
else
{
c.Add(new CuiLabel { Text = { Text = val, FontSize = 13, Align = TextAnchor.MiddleCenter, Color = "0.40 0.40 0.38 1.0" },
RectTransform = { AnchorMin = "0.61 1", AnchorMax = "1 1", OffsetMin = $"0 {-(y+RH)}", OffsetMax = $"-6 {-y}" }}, SCROLL);
}
return y + RH + 1;
}
float ActionRow(CuiElementContainer c, bool en, float y)
{
const float h = 50f;
var acts = new (string lbl, string cmd, string col)[]
{
("強制オープン", "open", "0.20 0.50 0.28 1.0"),
("強制クローズ", "close", "0.55 0.18 0.12 1.0"),
("島を生成", "spawnisland", "0.30 0.44 0.22 1.0"),
("幽霊船を生成", "spawnghostship","0.40 0.28 0.50 1.0"),
("浮遊都市を生成", "spawncity", "0.22 0.40 0.50 1.0"),
("RHIBグループを生成", "spawnrhib", "0.44 0.36 0.20 1.0"),
};
float bw = 1.0f / acts.Length;
for (int i = 0; i < acts.Length; i++)
{
float x0 = i * bw, x1 = x0 + bw;
string col = en ? acts[i].col : "0.20 0.20 0.18 1.0";
string tc = en ? "0.95 0.92 0.88 1.0" : "0.36 0.36 0.34 1.0";
string cmd = en ? $"dsm.action {acts[i].cmd}" : "";
c.Add(new CuiButton { Button = { Command = cmd, Color = col },
RectTransform = { AnchorMin = $"{x0:F4} 1", AnchorMax = $"{x1:F4} 1", OffsetMin = $"2 {-(y+h-3)}", OffsetMax = $"-2 {-(y+3)}" },
Text = { Text = acts[i].lbl, FontSize = 10, Align = TextAnchor.MiddleCenter, Color = tc }}, SCROLL);
}
if (!en)
{
c.Add(new CuiLabel { Text = { Text = "※ DeepSea有効が必要です", FontSize = 10, Align = TextAnchor.LowerCenter, Color = "0.92 0.70 0.18 0.8" },
RectTransform = { AnchorMin = "0 1", AnchorMax = "1 1", OffsetMin = $"0 {-(y+h+18)}", OffsetMax = $"0 {-(y+h)}" }}, SCROLL);
return y + h + 20;
}
return y + h + 4;
}
float StatusRow(CuiElementContainer c, float y)
{
const float h = 46f;
var btns = new (string lbl, string cmd, string col)[]
{
("ステータス確認", "status", "0.28 0.38 0.50 1.0"),
("エンティティ数", "entitycount", "0.35 0.42 0.28 1.0"),
("戦利品情報", "loot", "0.48 0.36 0.22 1.0"),
("ワイプ残り時間", "timetowipe", "0.38 0.28 0.48 1.0"),
};
float bw = 1.0f / btns.Length;
for (int i = 0; i < btns.Length; i++)
{
float x0 = i * bw, x1 = x0 + bw;
c.Add(new CuiButton { Button = { Command = $"dsm.action {btns[i].cmd}", Color = btns[i].col },
RectTransform = { AnchorMin = $"{x0:F4} 1", AnchorMax = $"{x1:F4} 1", OffsetMin = $"2 {-(y+h-3)}", OffsetMax = $"-2 {-(y+3)}" },
Text = { Text = btns[i].lbl, FontSize = 11, Align = TextAnchor.MiddleCenter, Color = "0.95 0.92 0.88 1.0" }}, SCROLL);
}
c.Add(new CuiLabel { Text = { Text = "※ 結果はチャットに表示されます", FontSize = 9, Align = TextAnchor.LowerCenter, Color = "0.58 0.56 0.52 0.9" },
RectTransform = { AnchorMin = "0 1", AnchorMax = "1 1", OffsetMin = $"0 {-(y+h+16)}", OffsetMax = $"0 {-(y+h)}" }}, SCROLL);
return y + h + 18;
}
}
}