using System; using System.Collections.Generic; using System.Globalization; using Newtonsoft.Json; using Oxide.Core; using Oxide.Core.Libraries.Covalence; using Oxide.Core.Plugins; namespace Oxide.Plugins { [Info("RustWebManager", "Mazurk4_", "1.10.0")] [Description("Rust Web Manager との連携プラグイン — 接続イベント記録・チャット履歴・kill/death ログ・チーム履歴・F7レポート・PM送信")] class RustWebManager : RustPlugin { // ── コンフィグ ──────────────────────────────────────────────────── private PluginConfig _config; class PluginConfig { [JsonProperty("SaveIntervalMinutes")] public int SaveIntervalMinutes { get; set; } = 5; } protected override void LoadDefaultConfig() { _config = new PluginConfig(); SaveConfig(); } protected override void LoadConfig() { base.LoadConfig(); try { _config = Config.ReadObject(); if (_config == null) LoadDefaultConfig(); _config.SaveIntervalMinutes = Math.Max(5, Math.Min(30, _config.SaveIntervalMinutes)); } catch { LoadDefaultConfig(); } } protected override void SaveConfig() { Config.WriteObject(_config); } // ── 接続イベント (時系列リスト) ────────────────────────────────── private const string EventDataFile = "RustWebManager/events"; private const int MaxEvents = 10000; private const int RetentionDays = 7; private List _events = new List(); class PlayerEvent { [JsonProperty("SteamId")] public string SteamId { get; set; } [JsonProperty("DisplayName")] public string DisplayName { get; set; } [JsonProperty("EventType")] public string EventType { get; set; } [JsonProperty("Timestamp")] public string Timestamp { get; set; } [JsonProperty("SessionSeconds", NullValueHandling = NullValueHandling.Ignore)] public int? SessionSeconds { get; set; } } // ── プレイヤーサマリ (SteamID ごとの集計) ─────────────────────── private const string PlayerDataFile = "RustWebManager/players"; private Dictionary _players = new Dictionary(); private Dictionary _sessionStart = new Dictionary(); class PlayerRecord { [JsonProperty("SteamID")] public string SteamID { get; set; } [JsonProperty("DisplayName")] public string DisplayName { get; set; } [JsonProperty("EventType")] public string EventType { get; set; } [JsonProperty("LastConnected")] public string LastConnected { get; set; } [JsonProperty("FirstConnected")] public string FirstConnected { get; set; } [JsonProperty("TotalConnections")] public int TotalConnections { get; set; } [JsonProperty("LastIP")] public string LastIP { get; set; } [JsonProperty("PlayTime")] public int PlayTime { get; set; } } // ── チャット履歴 ───────────────────────────────────────────────── private const string ChatDataFile = "RustWebManager/chat"; private const int MaxChatMessages = 10000; private const int ChatRetentionDays = RetentionDays; private List _chat = new List(); class ChatRecord { [JsonProperty("channel")] public int Channel { get; set; } [JsonProperty("team_id")] public string TeamId { get; set; } [JsonProperty("username")] public string Username { get; set; } [JsonProperty("user_id")] public string UserId { get; set; } [JsonProperty("color")] public string Color { get; set; } [JsonProperty("time")] public long Time { get; set; } [JsonProperty("message")] public string Message { get; set; } } // ── アクティビティログ (kill / death / chat) ───────────────────── private const string ActivityDataFile = "RustWebManager/activity"; private const int MaxActivity = 10000; private List _activity = new List(); class ActivityRecord { [JsonProperty("steam_id")] public string SteamId { get; set; } [JsonProperty("display_name")] public string DisplayName { get; set; } /// kill / death / chat [JsonProperty("activity_type")] public string ActivityType { get; set; } /// kill: 被キル者 SteamID, death: 加害者 SteamID (PVE/自殺時は null) [JsonProperty("target_steam_id")] public string TargetSteamId { get; set; } /// kill: 被キル者名, death: 加害者名 [JsonProperty("target_name")] public string TargetName { get; set; } /// kill/death: 武器名, chat: メッセージ本文 [JsonProperty("extra_data")] public string ExtraData { get; set; } [JsonProperty("recorded_at")] public string RecordedAt { get; set; } } // ── チーム / グループ情報 ─────────────────────────────────────── private const string TeamStateDataFile = "RustWebManager/team_state"; private const string TeamEventDataFile = "RustWebManager/team_events"; private const string TeamSnapshotDataFile = "RustWebManager/team_snapshots"; private const int MaxTeamEvents = 10000; private const int MaxTeamSnapshots = 10000; private Dictionary> _knownTeamMembers = new Dictionary>(); private List _teamEvents = new List(); private List _teamSnapshots = new List(); class TeamMemberRecord { [JsonProperty("steam_id")] public string SteamId { get; set; } [JsonProperty("display_name")] public string DisplayName { get; set; } [JsonProperty("is_leader")] public bool IsLeader { get; set; } [JsonProperty("is_online")] public bool IsOnline { get; set; } } class TeamRecord { [JsonProperty("team_id")] public string TeamId { get; set; } [JsonProperty("leader_steam_id")] public string LeaderSteamId { get; set; } [JsonProperty("leader_name")] public string LeaderName { get; set; } [JsonProperty("member_count")] public int MemberCount { get; set; } [JsonProperty("online_count")] public int OnlineCount { get; set; } [JsonProperty("members")] public List Members { get; set; } = new List(); } class TeamActivityRecord { [JsonProperty("team_id")] public string TeamId { get; set; } [JsonProperty("event_type")] public string EventType { get; set; } [JsonProperty("steam_id")] public string SteamId { get; set; } [JsonProperty("display_name")] public string DisplayName { get; set; } [JsonProperty("recorded_at")] public string RecordedAt { get; set; } } class TeamSnapshotRecord { [JsonProperty("team_id")] public string TeamId { get; set; } [JsonProperty("member_count")] public int MemberCount { get; set; } [JsonProperty("online_count")] public int OnlineCount { get; set; } [JsonProperty("recorded_at")] public string RecordedAt { get; set; } } // ── F7 プレイヤーレポート ──────────────────────────────────────── private const string ReportDataFile = "RustWebManager/reports"; private const int MaxReports = 10000; private const string ReportLogPrefix = "[RWM_REPORT]"; private List _reports = new List(); class PlayerReportRecord { [JsonProperty("id")] public string Id { get; set; } [JsonProperty("reporter_name")] public string ReporterName { get; set; } [JsonProperty("reporter_steam_id")] public string ReporterSteamId { get; set; } [JsonProperty("reported_name")] public string ReportedName { get; set; } [JsonProperty("reported_steam_id")] public string ReportedSteamId { get; set; } [JsonProperty("subject")] public string Subject { get; set; } [JsonProperty("message")] public string Message { get; set; } [JsonProperty("report_type")] public string ReportType { get; set; } [JsonProperty("created_at")] public string CreatedAt { get; set; } } class AuthListsResponse { [JsonProperty("owners")] public List Owners { get; set; } = new List(); [JsonProperty("moderators")] public List Moderators { get; set; } = new List(); } // ── タイマー ───────────────────────────────────────────────────── private Timer _saveTimer; // ── 初期化 ──────────────────────────────────────────────────────── void Init() { _events = Interface.Oxide.DataFileSystem.ReadObject>(EventDataFile) ?? new List(); _players = Interface.Oxide.DataFileSystem.ReadObject>(PlayerDataFile) ?? new Dictionary(); _chat = Interface.Oxide.DataFileSystem.ReadObject>(ChatDataFile) ?? new List(); _activity = Interface.Oxide.DataFileSystem.ReadObject>(ActivityDataFile) ?? new List(); _knownTeamMembers = Interface.Oxide.DataFileSystem.ReadObject>>(TeamStateDataFile) ?? new Dictionary>(); _teamEvents = Interface.Oxide.DataFileSystem.ReadObject>(TeamEventDataFile) ?? new List(); _teamSnapshots = Interface.Oxide.DataFileSystem.ReadObject>(TeamSnapshotDataFile) ?? new List(); _reports = Interface.Oxide.DataFileSystem.ReadObject>(ReportDataFile) ?? new List(); PurgeExpiredRecords(); } // ── サーバー初期化 ──────────────────────────────────────────────── void OnServerInitialized() { bool dirty = false; foreach (var player in BasePlayer.activePlayerList) { var steamId = player.UserIDString; if (!_players.TryGetValue(steamId, out var record)) { var now = DateTime.UtcNow; record = new PlayerRecord { SteamID = steamId, DisplayName = player.displayName, EventType = "connected", LastConnected = now.ToString("o"), FirstConnected = now.ToString("o"), TotalConnections = 1, LastIP = "", PlayTime = 0, }; _players[steamId] = record; dirty = true; } if (!_sessionStart.ContainsKey(steamId)) _sessionStart[steamId] = DateTime.UtcNow; } if (dirty) Interface.Oxide.DataFileSystem.WriteObject(PlayerDataFile, _players); CaptureTeams(); float intervalSeconds = _config.SaveIntervalMinutes * 60f; _saveTimer = timer.Every(intervalSeconds, SaveAll); Puts($"[RWM] Auto-save timer started: every {_config.SaveIntervalMinutes} min(s)"); } // ── アンロード ──────────────────────────────────────────────────── void Unload() { _saveTimer?.Destroy(); var now = DateTime.UtcNow; foreach (var kv in _sessionStart) { if (_players.TryGetValue(kv.Key, out var record)) record.PlayTime += (int)(now - kv.Value).TotalSeconds; } PurgeExpiredRecords(); Interface.Oxide.DataFileSystem.WriteObject(EventDataFile, _events); Interface.Oxide.DataFileSystem.WriteObject(PlayerDataFile, _players); Interface.Oxide.DataFileSystem.WriteObject(ChatDataFile, _chat); Interface.Oxide.DataFileSystem.WriteObject(ActivityDataFile, _activity); Interface.Oxide.DataFileSystem.WriteObject(TeamStateDataFile, _knownTeamMembers); Interface.Oxide.DataFileSystem.WriteObject(TeamEventDataFile, _teamEvents); Interface.Oxide.DataFileSystem.WriteObject(TeamSnapshotDataFile, _teamSnapshots); Interface.Oxide.DataFileSystem.WriteObject(ReportDataFile, _reports); } // ── 定期保存 ───────────────────────────────────────────────────── void SaveAll() { CaptureTeams(); PurgeExpiredRecords(); Interface.Oxide.DataFileSystem.WriteObject(EventDataFile, _events); Interface.Oxide.DataFileSystem.WriteObject(PlayerDataFile, _players); Interface.Oxide.DataFileSystem.WriteObject(ChatDataFile, _chat); Interface.Oxide.DataFileSystem.WriteObject(ActivityDataFile, _activity); Interface.Oxide.DataFileSystem.WriteObject(TeamStateDataFile, _knownTeamMembers); Interface.Oxide.DataFileSystem.WriteObject(TeamEventDataFile, _teamEvents); Interface.Oxide.DataFileSystem.WriteObject(TeamSnapshotDataFile, _teamSnapshots); Interface.Oxide.DataFileSystem.WriteObject(ReportDataFile, _reports); } // ── 接続イベントフック ──────────────────────────────────────────── void OnPlayerConnected(BasePlayer player) { var now = DateTime.UtcNow; var steamId = player.UserIDString; RecordEvent(player, "connected"); _sessionStart[steamId] = now; if (!_players.TryGetValue(steamId, out var record)) { record = new PlayerRecord { SteamID = steamId, FirstConnected = now.ToString("o"), PlayTime = 0, TotalConnections = 0, }; _players[steamId] = record; } var rawIp = player.net?.connection?.ipaddress ?? ""; record.DisplayName = player.displayName; record.EventType = "connected"; record.LastConnected = now.ToString("o"); record.TotalConnections += 1; record.LastIP = rawIp.Contains(":") ? rawIp.Split(':')[0] : rawIp; Interface.Oxide.DataFileSystem.WriteObject(PlayerDataFile, _players); } void OnPlayerDisconnected(BasePlayer player, string reason) { var now = DateTime.UtcNow; var steamId = player.UserIDString; int? sessionSec = null; if (_sessionStart.TryGetValue(steamId, out var start)) sessionSec = (int)(now - start).TotalSeconds; RecordEvent(player, "disconnected", sessionSec); if (_players.TryGetValue(steamId, out var record)) { if (sessionSec.HasValue) { record.PlayTime += sessionSec.Value; _sessionStart.Remove(steamId); } record.DisplayName = player.displayName; record.EventType = "disconnected"; Interface.Oxide.DataFileSystem.WriteObject(PlayerDataFile, _players); } } void RecordEvent(BasePlayer player, string eventType, int? sessionSeconds = null) { _events.Add(new PlayerEvent { SteamId = player.UserIDString, DisplayName = player.displayName, EventType = eventType, Timestamp = DateTime.UtcNow.ToString("o"), SessionSeconds = sessionSeconds, }); if (_events.Count > MaxEvents) _events.RemoveRange(0, _events.Count - MaxEvents); PurgeExpiredRecords(); Interface.Oxide.DataFileSystem.WriteObject(EventDataFile, _events); } // ── kill / death フック ─────────────────────────────────────────── // BasePlayer が死亡したときに呼ばれる。 // 加害者がいる場合: 被キル者に death・加害者に kill をそれぞれ記録する。 void OnEntityDeath(BaseCombatEntity entity, HitInfo info) { var victim = entity as BasePlayer; if (victim == null || victim.IsNpc) return; var attacker = info?.Initiator as BasePlayer; var now = DateTime.UtcNow.ToString("o"); var weapon = info?.WeaponPrefab?.ShortPrefabName ?? ""; // 被キル者の death イベント AddActivity(new ActivityRecord { SteamId = victim.UserIDString, DisplayName = victim.displayName, ActivityType = "death", TargetSteamId = attacker != null && !attacker.IsNpc ? attacker.UserIDString : null, TargetName = attacker != null && !attacker.IsNpc ? attacker.displayName : null, ExtraData = weapon, RecordedAt = now, }); // PVP: 加害者の kill イベント if (attacker != null && !attacker.IsNpc && attacker.UserIDString != victim.UserIDString) { AddActivity(new ActivityRecord { SteamId = attacker.UserIDString, DisplayName = attacker.displayName, ActivityType = "kill", TargetSteamId = victim.UserIDString, TargetName = victim.displayName, ExtraData = weapon, RecordedAt = now, }); } } private void AddActivity(ActivityRecord rec) { _activity.Add(rec); PurgeExpiredRecords(); if (_activity.Count > MaxActivity) _activity.RemoveRange(0, _activity.Count - MaxActivity); } // ── チャットフック ──────────────────────────────────────────────── void OnPlayerChat(BasePlayer player, string message, object channel) { if (player == null) return; int channelInt = NormalizeChatChannel(channel); RecordChat(channelInt, GetPlayerTeamId(player.userID), player.displayName, player.UserIDString, "#5af", message); // アクティビティログにもチャットを記録 AddActivity(new ActivityRecord { SteamId = player.UserIDString, DisplayName = player.displayName, ActivityType = "chat", ExtraData = message, RecordedAt = DateTime.UtcNow.ToString("o"), }); } private int NormalizeChatChannel(object channel) { if (channel == null) return 0; try { return Convert.ToInt32(channel); } catch { } var raw = channel.ToString(); if (string.IsNullOrEmpty(raw)) return 0; if (raw.IndexOf("Team", StringComparison.OrdinalIgnoreCase) >= 0) return 1; if (raw.IndexOf("Server", StringComparison.OrdinalIgnoreCase) >= 0) return 2; return 0; } private void RecordChat(int channel, string teamId, string username, string userId, string color, string message) { _chat.Add(new ChatRecord { Channel = channel, TeamId = teamId, Username = username, UserId = userId, Color = color, Time = DateTimeOffset.UtcNow.ToUnixTimeSeconds(), Message = message, }); var cutoff = DateTimeOffset.UtcNow.AddDays(-ChatRetentionDays).ToUnixTimeSeconds(); _chat.RemoveAll(r => r.Time < cutoff); if (_chat.Count > MaxChatMessages) _chat.RemoveRange(0, _chat.Count - MaxChatMessages); Interface.Oxide.DataFileSystem.WriteObject(ChatDataFile, _chat); } // ── チーム / グループ収集 ─────────────────────────────────────── private List GetCurrentTeams() { var result = new List(); var teams = RelationshipManager.ServerInstance?.teams; if (teams == null) return result; foreach (var kv in teams) { var team = kv.Value; if (team == null || team.members == null || team.members.Count == 0) continue; var teamId = kv.Key.ToString(); var leaderId = team.teamLeader.ToString(); var record = new TeamRecord { TeamId = teamId, LeaderSteamId = leaderId, LeaderName = ResolvePlayerName(leaderId), }; foreach (var memberId in team.members) { var steamId = memberId.ToString(); var online = IsPlayerOnline(memberId); record.Members.Add(new TeamMemberRecord { SteamId = steamId, DisplayName = ResolvePlayerName(steamId), IsLeader = steamId == leaderId, IsOnline = online, }); if (online) record.OnlineCount += 1; } record.MemberCount = record.Members.Count; result.Add(record); } return result; } private void CaptureTeams() { var now = DateTime.UtcNow.ToString("o"); var current = GetCurrentTeams(); var currentMap = new Dictionary>(); foreach (var team in current) { var members = new List(); foreach (var member in team.Members) members.Add(member.SteamId); currentMap[team.TeamId] = members; if (!_knownTeamMembers.ContainsKey(team.TeamId)) AddTeamEvent(team.TeamId, "created", team.LeaderSteamId, team.LeaderName, now); var previous = _knownTeamMembers.ContainsKey(team.TeamId) ? new HashSet(_knownTeamMembers[team.TeamId]) : new HashSet(); foreach (var member in team.Members) { if (!previous.Contains(member.SteamId)) AddTeamEvent(team.TeamId, "joined", member.SteamId, member.DisplayName, now); } foreach (var oldSteamId in previous) { if (!members.Contains(oldSteamId)) AddTeamEvent(team.TeamId, "left", oldSteamId, ResolvePlayerName(oldSteamId), now); } _teamSnapshots.Add(new TeamSnapshotRecord { TeamId = team.TeamId, MemberCount = team.MemberCount, OnlineCount = team.OnlineCount, RecordedAt = now, }); } foreach (var oldTeam in _knownTeamMembers) { if (currentMap.ContainsKey(oldTeam.Key)) continue; foreach (var oldSteamId in oldTeam.Value) AddTeamEvent(oldTeam.Key, "left", oldSteamId, ResolvePlayerName(oldSteamId), now); } _knownTeamMembers = currentMap; if (_teamSnapshots.Count > MaxTeamSnapshots) _teamSnapshots.RemoveRange(0, _teamSnapshots.Count - MaxTeamSnapshots); PurgeExpiredRecords(); } private void AddTeamEvent(string teamId, string eventType, string steamId, string displayName, string recordedAt) { _teamEvents.Add(new TeamActivityRecord { TeamId = teamId, EventType = eventType, SteamId = steamId, DisplayName = displayName, RecordedAt = recordedAt, }); PurgeExpiredRecords(); if (_teamEvents.Count > MaxTeamEvents) _teamEvents.RemoveRange(0, _teamEvents.Count - MaxTeamEvents); } private bool IsPlayerOnline(ulong steamId) { var player = BasePlayer.FindByID(steamId); return player != null && player.IsConnected; } private string GetPlayerTeamId(ulong steamId) { var teams = RelationshipManager.ServerInstance?.teams; if (teams == null) return null; foreach (var kv in teams) { var team = kv.Value; if (team?.members == null) continue; if (team.members.Contains(steamId)) return kv.Key.ToString(); } return null; } private string ResolvePlayerName(string steamId) { if (ulong.TryParse(steamId, out ulong id)) { var online = BasePlayer.FindByID(id); if (online != null && !string.IsNullOrEmpty(online.displayName)) return online.displayName; var sleeping = BasePlayer.FindSleeping(id); if (sleeping != null && !string.IsNullOrEmpty(sleeping.displayName)) return sleeping.displayName; } if (_players.TryGetValue(steamId, out var record) && !string.IsNullOrEmpty(record.DisplayName)) return record.DisplayName; var covalencePlayer = covalence.Players.FindPlayerById(steamId); return covalencePlayer?.Name ?? steamId; } // ── F7 レポートフック ──────────────────────────────────────────── void OnPlayerReported(BasePlayer reporter, string targetName, string targetId, string subject, string message, string reportType) { if (reporter == null) return; var rec = new PlayerReportRecord { Id = Guid.NewGuid().ToString("N"), ReporterName = reporter.displayName ?? "", ReporterSteamId = reporter.UserIDString ?? "", ReportedName = targetName ?? "", ReportedSteamId = targetId ?? "", Subject = subject ?? "", Message = message ?? "", ReportType = reportType ?? "", CreatedAt = DateTime.UtcNow.ToString("o"), }; _reports.Add(rec); PurgeExpiredRecords(); if (_reports.Count > MaxReports) _reports.RemoveRange(0, _reports.Count - MaxReports); Interface.Oxide.DataFileSystem.WriteObject(ReportDataFile, _reports); // RWM の console WS が開いている間は、この構造化ログから即時取り込みできる。 Puts($"{ReportLogPrefix} {JsonConvert.SerializeObject(rec)}"); } private void PurgeExpiredRecords() { var cutoff = DateTimeOffset.UtcNow.AddDays(-RetentionDays); _events.RemoveAll(r => IsOlderThan(r.Timestamp, cutoff)); _chat.RemoveAll(r => r.Time < cutoff.ToUnixTimeSeconds()); _activity.RemoveAll(r => IsOlderThan(r.RecordedAt, cutoff)); _teamEvents.RemoveAll(r => IsOlderThan(r.RecordedAt, cutoff)); _teamSnapshots.RemoveAll(r => IsOlderThan(r.RecordedAt, cutoff)); _reports.RemoveAll(r => IsOlderThan(r.CreatedAt, cutoff)); var playersToRemove = new List(); foreach (var kv in _players) if (!_sessionStart.ContainsKey(kv.Key) && IsOlderThan(kv.Value.LastConnected, cutoff)) playersToRemove.Add(kv.Key); foreach (var key in playersToRemove) _players.Remove(key); } private bool IsOlderThan(string value, DateTimeOffset cutoff) { if (string.IsNullOrEmpty(value)) return false; DateTimeOffset timestamp; if (!DateTimeOffset.TryParse(value, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out timestamp)) return false; return timestamp < cutoff; } // ── コンソールコマンド ──────────────────────────────────────────── [ConsoleCommand("rwm.ping")] void CmdPing(ConsoleSystem.Arg arg) { arg.ReplyWith("pong"); } [ConsoleCommand("rwm.version")] void CmdVersion(ConsoleSystem.Arg arg) { arg.ReplyWith("1.10.0"); } [ConsoleCommand("rwm.getevents")] void CmdGetEvents(ConsoleSystem.Arg arg) { arg.ReplyWith(JsonConvert.SerializeObject(_events)); } [ConsoleCommand("rwm.getplayers")] void CmdGetPlayers(ConsoleSystem.Arg arg) { arg.ReplyWith(JsonConvert.SerializeObject(_players)); } /// /// rwm.getteams /// 現在存在する Rust チームとメンバーを返す。 /// [ConsoleCommand("rwm.getteams")] void CmdGetTeams(ConsoleSystem.Arg arg) { CaptureTeams(); arg.ReplyWith(JsonConvert.SerializeObject(new { teams = GetCurrentTeams(), })); } /// /// rwm.getteamevents [limit=200] [offset=0] /// チーム作成・参加・離脱履歴を新しい順に返す。 /// [ConsoleCommand("rwm.getteamevents")] void CmdGetTeamEvents(ConsoleSystem.Arg arg) { int limit = 200; int offset = 0; if (arg.Args != null && arg.Args.Length >= 1) int.TryParse(arg.Args[0], out limit); if (arg.Args != null && arg.Args.Length >= 2) int.TryParse(arg.Args[1], out offset); limit = Math.Max(1, Math.Min(limit, 500)); offset = Math.Max(0, offset); int total = _teamEvents.Count; int end = Math.Max(0, total - offset); int start = Math.Max(0, end - limit); int count = end - start; var slice = count > 0 ? _teamEvents.GetRange(start, count) : new List(); arg.ReplyWith(JsonConvert.SerializeObject(new { total = total, offset = offset, events = slice, })); } /// /// rwm.getteamsnapshots [limit=500] [offset=0] /// チーム別オンライン人数スナップショットを新しい順に返す。 /// [ConsoleCommand("rwm.getteamsnapshots")] void CmdGetTeamSnapshots(ConsoleSystem.Arg arg) { int limit = 500; int offset = 0; if (arg.Args != null && arg.Args.Length >= 1) int.TryParse(arg.Args[0], out limit); if (arg.Args != null && arg.Args.Length >= 2) int.TryParse(arg.Args[1], out offset); limit = Math.Max(1, Math.Min(limit, 1000)); offset = Math.Max(0, offset); int total = _teamSnapshots.Count; int end = Math.Max(0, total - offset); int start = Math.Max(0, end - limit); int count = end - start; var slice = count > 0 ? _teamSnapshots.GetRange(start, count) : new List(); arg.ReplyWith(JsonConvert.SerializeObject(new { total = total, offset = offset, snapshots = slice, })); } /// /// rwm.getauths /// Returns owner/moderator steam ids as JSON. /// [ConsoleCommand("rwm.getauths")] void CmdGetAuths(ConsoleSystem.Arg arg) { var result = new AuthListsResponse(); try { var owners = ServerUsers.GetAll(ServerUsers.UserGroup.Owner); if (owners != null) { foreach (var u in owners) { if (u == null) continue; result.Owners.Add(u.steamid.ToString()); } } var moderators = ServerUsers.GetAll(ServerUsers.UserGroup.Moderator); if (moderators != null) { foreach (var u in moderators) { if (u == null) continue; result.Moderators.Add(u.steamid.ToString()); } } } catch (Exception e) { Puts($"[RWM] rwm.getauths failed: {e.Message}"); } arg.ReplyWith(JsonConvert.SerializeObject(result)); } /// /// rwm.getactivity [steam_id] [limit=100] [offset=0] /// steam_id を省略すると全プレイヤーの最新 limit 件を返す。 /// [ConsoleCommand("rwm.getactivity")] void CmdGetActivity(ConsoleSystem.Arg arg) { string steamId = null; int limit = 100; int offset = 0; if (arg.Args != null && arg.Args.Length >= 1) steamId = arg.Args[0]; if (arg.Args != null && arg.Args.Length >= 2) int.TryParse(arg.Args[1], out limit); if (arg.Args != null && arg.Args.Length >= 3) int.TryParse(arg.Args[2], out offset); limit = Math.Max(1, Math.Min(limit, 500)); offset = Math.Max(0, offset); List source = steamId != null ? _activity.FindAll(r => r.SteamId == steamId) : _activity; int total = source.Count; int end = Math.Max(0, total - offset); int start = Math.Max(0, end - limit); int count = end - start; var slice = count > 0 ? source.GetRange(start, count) : new List(); arg.ReplyWith(JsonConvert.SerializeObject(new { total = total, offset = offset, events = slice, })); } /// /// rwm.getreports [limit=200] [offset=0] /// F7 レポートを新しい順に返す。 /// [ConsoleCommand("rwm.getreports")] void CmdGetReports(ConsoleSystem.Arg arg) { int limit = 200; int offset = 0; if (arg.Args != null && arg.Args.Length >= 1) int.TryParse(arg.Args[0], out limit); if (arg.Args != null && arg.Args.Length >= 2) int.TryParse(arg.Args[1], out offset); limit = Math.Max(1, Math.Min(limit, 500)); offset = Math.Max(0, offset); int total = _reports.Count; int end = Math.Max(0, total - offset); int start = Math.Max(0, end - limit); int count = end - start; var slice = count > 0 ? _reports.GetRange(start, count) : new List(); arg.ReplyWith(JsonConvert.SerializeObject(new { total = total, offset = offset, reports = slice, })); } /// /// rwm.getchat [limit=100] [offset=0] /// [ConsoleCommand("rwm.getchat")] void CmdGetChat(ConsoleSystem.Arg arg) { int limit = 100; int offset = 0; if (arg.Args != null && arg.Args.Length >= 1) int.TryParse(arg.Args[0], out limit); if (arg.Args != null && arg.Args.Length >= 2) int.TryParse(arg.Args[1], out offset); limit = Math.Max(1, Math.Min(limit, 500)); offset = Math.Max(0, offset); int total = _chat.Count; int end = Math.Max(0, total - offset); int start = Math.Max(0, end - limit); int count = end - start; var slice = count > 0 ? _chat.GetRange(start, count) : new List(); arg.ReplyWith(JsonConvert.SerializeObject(new { total = total, offset = offset, messages = slice, })); } [ConsoleCommand("rwm.clearchat")] void CmdClearChat(ConsoleSystem.Arg arg) { _chat.Clear(); Interface.Oxide.DataFileSystem.WriteObject(ChatDataFile, _chat); arg.ReplyWith("ok"); } [ConsoleCommand("rwm.clearall")] void CmdClearAll(ConsoleSystem.Arg arg) { _events.Clear(); _chat.Clear(); _activity.Clear(); _players.Clear(); _knownTeamMembers.Clear(); _teamEvents.Clear(); _teamSnapshots.Clear(); _reports.Clear(); // 切断時に TryGetValue が失敗しないよう、現在接続中のプレイヤーを再登録する var now = DateTime.UtcNow; foreach (var player in BasePlayer.activePlayerList) { var sid = player.UserIDString; var rawIp = player.net?.connection?.ipaddress ?? ""; _players[sid] = new PlayerRecord { SteamID = sid, DisplayName = player.displayName, EventType = "connected", FirstConnected = now.ToString("o"), LastConnected = now.ToString("o"), TotalConnections = 1, PlayTime = 0, LastIP = rawIp.Contains(":") ? rawIp.Split(':')[0] : rawIp, }; } Interface.Oxide.DataFileSystem.WriteObject(EventDataFile, _events); Interface.Oxide.DataFileSystem.WriteObject(ChatDataFile, _chat); Interface.Oxide.DataFileSystem.WriteObject(ActivityDataFile, _activity); Interface.Oxide.DataFileSystem.WriteObject(PlayerDataFile, _players); Interface.Oxide.DataFileSystem.WriteObject(TeamStateDataFile, _knownTeamMembers); Interface.Oxide.DataFileSystem.WriteObject(TeamEventDataFile, _teamEvents); Interface.Oxide.DataFileSystem.WriteObject(TeamSnapshotDataFile, _teamSnapshots); Interface.Oxide.DataFileSystem.WriteObject(ReportDataFile, _reports); arg.ReplyWith("ok"); } [ConsoleCommand("rwm.pm")] void CmdPm(ConsoleSystem.Arg arg) { if (arg.Args == null || arg.Args.Length < 2) { arg.ReplyWith("Usage: rwm.pm "); return; } var steamIdStr = arg.Args[0]; if (!ulong.TryParse(steamIdStr, out ulong steamId)) { arg.ReplyWith($"Invalid SteamID: {steamIdStr}"); return; } var message = string.Join(" ", arg.Args, 1, arg.Args.Length - 1); var target = BasePlayer.FindByID(steamId); if (target == null || !target.IsConnected) { arg.ReplyWith($"Player {steamIdStr} is not online"); return; } target.ChatMessage($"[Server PM] {message}"); arg.ReplyWith($"Message sent to {target.displayName}"); } /// rwm.give [ConsoleCommand("rwm.give")] void CmdGive(ConsoleSystem.Arg arg) { if (arg.Args == null || arg.Args.Length < 3) { arg.ReplyWith("Usage: rwm.give "); return; } if (!ulong.TryParse(arg.Args[0], out ulong steamId)) { arg.ReplyWith($"Invalid SteamID: {arg.Args[0]}"); return; } var shortname = arg.Args[1]; if (string.IsNullOrEmpty(shortname)) { arg.ReplyWith("Invalid item shortname"); return; } if (!int.TryParse(arg.Args[2], out int amount) || amount <= 0) { arg.ReplyWith($"Invalid amount: {arg.Args[2]}"); return; } var player = BasePlayer.FindByID(steamId); if (player == null || !player.IsConnected) { arg.ReplyWith($"Player {steamId} is not online"); return; } var item = ItemManager.CreateByName(shortname, amount); if (item == null) { arg.ReplyWith($"Item not found: {shortname}"); return; } player.GiveItem(item); arg.ReplyWith($"ok: gave {amount} {shortname} to {player.displayName}"); } /// rwm.getinventory /// Returns Items/Worn/Belt as JSON arrays in the format expected by the backend parser. [ConsoleCommand("rwm.getinventory")] void CmdGetInventory(ConsoleSystem.Arg arg) { if (arg.Args == null || arg.Args.Length < 1) { arg.ReplyWith("Usage: rwm.getinventory "); return; } if (!ulong.TryParse(arg.Args[0], out ulong steamId)) { arg.ReplyWith($"Invalid SteamID: {arg.Args[0]}"); return; } var player = BasePlayer.FindByID(steamId); if (player == null || !player.IsConnected) { arg.ReplyWith($"Player {steamId} is not online"); return; } var items = SerializeContainer(player.inventory.containerMain); var worn = SerializeContainer(player.inventory.containerWear); var belt = SerializeContainer(player.inventory.containerBelt); arg.ReplyWith( $"Items: {JsonConvert.SerializeObject(items)}\n" + $"Worn: {JsonConvert.SerializeObject(worn)}\n" + $"Belt: {JsonConvert.SerializeObject(belt)}" ); } private List> SerializeContainer(ItemContainer container) { var list = new List>(); if (container == null) return list; foreach (var item in container.itemList) { list.Add(SerializeItem(item)); } return list; } private Dictionary SerializeItem(Item item) { int ammo = 0; int ammotype = -1; var gun = item.GetHeldEntity() as BaseProjectile; if (gun != null) { ammo = gun.primaryMagazine.contents; ammotype = gun.primaryMagazine.ammoType?.itemid ?? -1; } var row = new Dictionary { ["itemid"] = item.info.itemid, ["amount"] = item.amount, ["skinid"] = item.skin, ["slot"] = item.position, ["ammo"] = ammo, ["ammotype"] = ammotype, ["condition"] = item.condition, ["maxcondition"] = item.maxCondition, ["flags"] = (int)item.flags, ["name"] = item.info.displayName.translated, ["shortname"] = item.info.shortname, ["blueprint"] = item.IsBlueprint(), }; if (item.contents != null && item.contents.itemList != null && item.contents.itemList.Count > 0) { row["contents"] = SerializeContainer(item.contents); } return row; } } }