using System; using System.Collections.Generic; using UnityEngine; namespace Oxide.Plugins { [Info("薯片工具助手 ServerToolHelper", "薯片", "1.0.3")] [Description("薯片服务器管理工具配套插件:在玩家位置生成实体、查询玩家坐标、超能力管理(无敌/飞行/治疗——原版RCON做不到的事由它补上)")] class ServerToolHelper : RustPlugin { // 回话约定:OK| 成功 / ERR| 失败 / POS| 坐标——管理工具靠前缀识别结果 private void Init() { Puts("薯片工具助手已加载 v1.0.3(提供 stool.ping / stool.pos / stool.spawn / stool.resolve / stool.god / stool.noclip / stool.heal)"); } // stool.ping —— 管理工具探测插件是否已安装 [ConsoleCommand("stool.ping")] private void CmdPing(ConsoleSystem.Arg arg) { arg.ReplyWith("OK|pong 薯片工具助手 v1.0.3 在线"); } // stool.pos <玩家名或SteamID> —— 查询在线玩家当前坐标 [ConsoleCommand("stool.pos")] private void CmdPos(ConsoleSystem.Arg arg) { string name = arg.GetString(0, ""); if (string.IsNullOrEmpty(name)) { arg.ReplyWith("ERR|用法:stool.pos <玩家名或SteamID>"); return; } BasePlayer p = FindPlayer(name); if (p == null) { arg.ReplyWith("ERR|找不到在线玩家:" + name); return; } Vector3 v = p.transform.position; arg.ReplyWith(string.Format("POS|{0}|{1:F1}|{2:F1}|{3:F1}", p.displayName, v.x, v.y, v.z)); } // stool.resolve <实体名> —— 只解析不生成:返回短名对应的完整 prefab 路径; // 解析失败时给出清单里含关键词的候选(像原版 spawn 的 could be 提示) [ConsoleCommand("stool.resolve")] private void CmdResolve(ConsoleSystem.Arg arg) { string name = arg.GetString(0, ""); if (string.IsNullOrEmpty(name)) { arg.ReplyWith("ERR|用法:stool.resolve <实体名或assets/路径>"); return; } string path = ResolvePrefab(name); if (path != null) { arg.ReplyWith("OK|" + name + " => " + path); return; } string lower = name.ToLowerInvariant(); List cand = new List(); foreach (string e in GameManifest.Current.entities) { if (e.ToLowerInvariant().Contains(lower)) { cand.Add(e); if (cand.Count >= 5) break; } } arg.ReplyWith("ERR|未找到实体:" + name + (cand.Count > 0 ? ",候选:" + string.Join(" ; ", cand.ToArray()) : "(实体清单里也没有含此关键词的项)")); } // stool.spawn <实体名> <玩家名或SteamID> [数量1-10] —— 在玩家面朝方向2米处生成实体 [ConsoleCommand("stool.spawn")] private void CmdSpawn(ConsoleSystem.Arg arg) { // 游戏内只允许管理员用;RCON 调用时 Player() 为 null 直接放行 BasePlayer caller = arg.Player(); if (caller != null && !caller.IsAdmin) { arg.ReplyWith("ERR|只有管理员可以使用此命令"); return; } string entity = arg.GetString(0, ""); string pname = arg.GetString(1, ""); int amount = Math.Max(1, Math.Min(arg.GetInt(2, 1), 10)); if (string.IsNullOrEmpty(entity) || string.IsNullOrEmpty(pname)) { arg.ReplyWith("ERR|用法:stool.spawn <实体名> <玩家名或SteamID> [数量1-10]"); return; } BasePlayer target = FindPlayer(pname); if (target == null) { arg.ReplyWith("ERR|找不到在线玩家:" + pname + "(刷实体需要玩家在线)"); return; } // 短名 → 完整 prefab 路径(CreateEntity 只认 assets/ 开头的路径, // 直接传短名会报 LoadPrefab - should start with assets/ 并生成失败) string prefabPath = ResolvePrefab(entity); if (prefabPath == null) { arg.ReplyWith("ERR|找不到实体:" + entity + "(可用 stool.resolve " + entity + " 查看候选名)"); return; } // 玩家面朝方向(压平到水平面,防止朝天/朝地生成) Vector3 forward = target.eyes.BodyForward(); forward.y = 0f; if (forward.sqrMagnitude < 0.001f) forward = target.transform.forward; forward.Normalize(); Vector3 side = Vector3.Cross(Vector3.up, forward); int ok = 0; for (int i = 0; i < amount; i++) { // 多个实体横向排开,避免叠在一起 float spread = amount > 1 ? (i - (amount - 1) / 2f) * 1.8f : 0f; Vector3 pos = target.transform.position + forward * 2.2f + side * spread + Vector3.up * 0.3f; BaseEntity ent = null; try { ent = GameManager.server.CreateEntity(prefabPath, pos, Quaternion.LookRotation(forward)); } catch (Exception) { } if (ent == null) { arg.ReplyWith("ERR|生成失败:" + entity + "(路径 " + prefabPath + " 创建实体返回空)"); return; } ent.Spawn(); ok++; } Vector3 tp = target.transform.position; arg.ReplyWith(string.Format("OK|已在 {0} 旁生成 {1} ×{2}(玩家坐标 {3:F0}, {4:F0}, {5:F0})", target.displayName, entity, ok, tp.x, tp.y, tp.z)); } // 短名 → assets/ 完整路径解析: // 1) 已是 assets/ 开头直接用;2) 实体清单按「文件名去掉 .prefab」精确匹配(大小写不敏感); // 3) 精确无果时文件名前缀匹配兜底(如 wolf → wolf2)。清单以本服务器为准,不怕版本变动。 // 返回值统一转小写:Rust 内部对混合大小写路径会打印 Converting prefab name 日志, // 该日志会混进 RCON 回话流把真正的命令回话顶掉,提前转小写保持回话干净。 private static string ResolvePrefab(string name) { if (string.IsNullOrEmpty(name)) return null; if (name.StartsWith("assets/", StringComparison.OrdinalIgnoreCase)) return name.ToLowerInvariant(); string lower = name.ToLowerInvariant(); string partial = null; foreach (string e in GameManifest.Current.entities) { string file = e; int slash = e.LastIndexOf('/'); if (slash >= 0) file = e.Substring(slash + 1); string noExt = file.EndsWith(".prefab", StringComparison.OrdinalIgnoreCase) ? file.Substring(0, file.Length - 7) : file; if (string.Equals(noExt, lower, StringComparison.OrdinalIgnoreCase) || string.Equals(file, lower, StringComparison.OrdinalIgnoreCase)) return e.ToLowerInvariant(); if (partial == null && noExt.StartsWith(lower, StringComparison.OrdinalIgnoreCase)) partial = e; } return partial == null ? null : partial.ToLowerInvariant(); } // ===================== v1.0.3 超能力(god / noclip / heal) ===================== // 背景:这三个在游戏里是 F1 客户端命令,服务器 RCON 侧根本不存在 // (god/noclip 未注册 → RCON 无回话超时;heal 存在但按在线玩家执行者设计, // RCON 调用 arg.Player() 为 null → 直接 NullReferenceException)。 // 所以跟 spawn 一样,只能由插件接管。 // 无敌名单:伤害钩子实时判定。内存态——插件重载后清空,工具里再开一次即可 private readonly HashSet _gods = new HashSet(); // 飞行名单仅作状态展示用的「尽力记录」:真实开关在玩家客户端,死亡/重进自动消失 private readonly HashSet _flyers = new HashSet(); // stool.god <玩家名或SteamID> [1开/0关,省略=切换] —— 无敌:所有伤害归零 [ConsoleCommand("stool.god")] private void CmdGod(ConsoleSystem.Arg arg) { BasePlayer caller = arg.Player(); if (caller != null && !caller.IsAdmin) { arg.ReplyWith("ERR|只有管理员可以使用此命令"); return; } string pname = arg.GetString(0, ""); if (string.IsNullOrEmpty(pname)) { arg.ReplyWith("ERR|用法:stool.god <玩家名或SteamID> [1开/0关]"); return; } BasePlayer target = FindPlayer(pname); if (target == null) { arg.ReplyWith("ERR|找不到在线玩家:" + pname + "(无敌只对在线玩家有效)"); return; } string sw = arg.GetString(1, "").ToLowerInvariant(); bool want; if (sw == "1" || sw == "on" || sw == "true") want = true; else if (sw == "0" || sw == "off" || sw == "false") want = false; else want = !_gods.Contains(target.userID); // 省略 = 切换 if (want) { _gods.Add(target.userID); target.Heal(target.MaxHealth()); // 开启时顺手补满,体验对齐 F1 god 1 arg.ReplyWith("OK|无敌已开启:" + target.displayName + "(任何伤害都打不掉血;插件重载后需重开)"); } else { _gods.Remove(target.userID); arg.ReplyWith("OK|无敌已关闭:" + target.displayName); } } // 无敌核心:被打时所有伤害类型归零 private object OnEntityTakeDamage(BaseCombatEntity entity, HitInfo info) { BasePlayer victim = entity as BasePlayer; if (victim != null && _gods.Contains(victim.userID)) { if (info != null && info.damageTypes != null) info.damageTypes.ScaleAll(0f); return true; } return null; } // stool.noclip <玩家名或SteamID> —— 飞行开关(再点一次关闭) [ConsoleCommand("stool.noclip")] private void CmdNoclip(ConsoleSystem.Arg arg) { BasePlayer caller = arg.Player(); if (caller != null && !caller.IsAdmin) { arg.ReplyWith("ERR|只有管理员可以使用此命令"); return; } string pname = arg.GetString(0, ""); if (string.IsNullOrEmpty(pname)) { arg.ReplyWith("ERR|用法:stool.noclip <玩家名或SteamID>"); return; } BasePlayer target = FindPlayer(pname); if (target == null) { arg.ReplyWith("ERR|找不到在线玩家:" + pname + "(飞行只对在线玩家有效)"); return; } // noclip 是客户端命令、客户端只认 IsAdmin 标记才执行 → // 临时打标 → 下发 noclip → 收回标记(真管理员不收回,免得误摘人家的标) ulong uid = target.userID; bool wasAdmin = target.HasPlayerFlag(BasePlayer.PlayerFlags.IsAdmin); target.SetPlayerFlag(BasePlayer.PlayerFlags.IsAdmin, true); target.SendNetworkUpdateImmediate(); timer.Once(0.5f, () => { BasePlayer p = BasePlayer.FindByID(uid); if (p == null || !p.IsConnected) return; p.SendConsoleCommand("noclip"); if (!wasAdmin) { timer.Once(1f, () => { BasePlayer p2 = BasePlayer.FindByID(uid); if (p2 == null || !p2.IsConnected) return; p2.SetPlayerFlag(BasePlayer.PlayerFlags.IsAdmin, false); p2.SendNetworkUpdateImmediate(); }); } }); bool nowOn = !_flyers.Contains(uid); if (nowOn) _flyers.Add(uid); else _flyers.Remove(uid); arg.ReplyWith("OK|" + (nowOn ? "飞行已开启:" : "飞行已关闭:") + target.displayName + "(WASD+空格/下蹲控制升降;再点一次关闭;死亡或重进游戏也会自动关闭)"); } // 重生后客户端飞行必然已消失,同步清记录避免工具侧状态显示错乱 private void OnPlayerRespawned(BasePlayer player) { if (player != null) _flyers.Remove(player.userID); } // stool.heal <玩家名或SteamID> —— 治疗满血+止血 [ConsoleCommand("stool.heal")] private void CmdHeal(ConsoleSystem.Arg arg) { BasePlayer caller = arg.Player(); if (caller != null && !caller.IsAdmin) { arg.ReplyWith("ERR|只有管理员可以使用此命令"); return; } string pname = arg.GetString(0, ""); if (string.IsNullOrEmpty(pname)) { arg.ReplyWith("ERR|用法:stool.heal <玩家名或SteamID>"); return; } BasePlayer target = FindPlayer(pname); if (target == null) { arg.ReplyWith("ERR|找不到在线玩家:" + pname); return; } target.Heal(target.MaxHealth()); // 止血:BasePlayer 没有 StopBleeding、PlayerMetabolism 也没有 SendChangesToClient // (两次编译实锤),直接清零代谢层流血值即可,客户端由代谢系统自行同步 target.metabolism.bleeding.value = 0f; arg.ReplyWith("OK|已治疗满血:" + target.displayName); } // 按名字(模糊)或 SteamID 找在线玩家 private BasePlayer FindPlayer(string nameOrId) { ulong id; if (ulong.TryParse(nameOrId, out id)) { BasePlayer byId = BasePlayer.FindByID(id); if (byId != null) return byId; } return BasePlayer.Find(nameOrId); } } }