← Back to changelog

What changed

CargoSinks · v1.1.0 → v1.1.1 (current)

Diff 3 added · 2 removed · 418 → 419 total lines
1 1 /*▄▄▄ ███▄ ▄███▓ ▄████ ▄▄▄██▀▀▀▓█████▄▄▄█████▓
2 2 ▓█████▄ ▓██▒▀█▀ ██▒ ██▒ ▀█▒ ▒██ ▓█ ▀▓ ██▒ ▓▒
3 3 ▒██▒ ▄██▓██ ▓██░▒██░▄▄▄░ ░██ ▒███ ▒ ▓██░ ▒░
4 4 ▒██░█▀ ▒██ ▒██ ░▓█ ██▓▓██▄██▓ ▒▓█ ▄░ ▓██▓ ░
5 5 ░▓█ ▀█▓▒██▒ ░██▒░▒▓███▀▒ ▓███▒ ░▒████▒ ▒██▒ ░
6 6 ░▒▓███▀▒░ ▒░ ░ ░ ░▒ ▒ ▒▓▒▒░ ░░ ▒░ ░ ▒ ░░
7 7 ▒░▒ ░ ░ ░ ░ ░ ░ ▒ ░▒░ ░ ░ ░ ░
8 8 ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░
9 9 ░ ░ ░ ░ ░ ░ ░*/
10 10 using HarmonyLib;
11 11 using Newtonsoft.Json;
12 12 using Oxide.Core.Plugins;
13 13 using System;
14 14 using System.Collections.Generic;
15 15 using System.Linq;
16 16 using UnityEngine;
17 17
18 18 namespace Oxide.Plugins
19 19 {
20 - [Info("CargoSinks", "bmgjet", "1.1.0")]
20 + [Info("CargoSinks", "bmgjet", "1.1.1")]
21 21 class CargoSinks : RustPlugin
22 22 {
23 23 public static CargoSinks cargoSinks;
24 24 private const string c4 = "assets/prefabs/tools/c4/effects/c4_explosion.prefab";
25 25 private const string debris = "assets/content/vehicles/attackhelicopter/debris_effect.prefab";
26 26 private List<CargoShip> CargoKillShipList = new List<CargoShip>();
27 27 private Vector3[] SubEntities = new Vector3[] { new Vector3(0.55f, 9.58f, -61.38f), new Vector3(0.01f, 24.5f, -38.64f), new Vector3(9.62f, 9.14f, -16.50f), new Vector3(-9.62f, 9.14f, -16.50f), new Vector3(9.62f, 9.14f, 10.50f), new Vector3(-9.62f, 9.14f, 10.50f), new Vector3(9.62f, 9.14f, 37.50f), new Vector3(-9.62f, 9.14f, 37.50f) };
28 28
29 29 #region Configuration
30 30 private Configuration config;
31 31 private class Configuration
32 32 {
33 33 [JsonProperty("Show Console Debug Messages")]
34 34 public bool Debug = false;
35 35
36 36 [JsonProperty("Chat Command To Force-Sink Cargo Ship You're Looking At")]
37 37 public string SinkCommand = "sinkme";
38 38
39 39 [JsonProperty("Require Permission Instead Of Admin Only For Sink Command")]
40 40 public bool RequirePermission = false;
41 41
42 42 [JsonProperty("Permission Needed For Sink Command (Only If Above Is True)")]
43 43 public string SinkPermission = "cargosinks.admin";
44 44
45 45 [JsonProperty("Reverse Cargo Ship Patrol Path")]
46 46 public bool ReverseCargoPath = false;
47 47
48 48 [JsonProperty("Topology That Blocks Egress (Bitmask, Default = Monument)")]
49 49 public int TopologyBaseBlock = TerrainTopology.MONUMENT;
50 50
51 51 [JsonProperty("Retry Delay When Egress Blocked By Topology (Seconds)")]
52 52 public float TopologyBlockRetryDelay = 30f;
53 53
54 54 [JsonProperty("Radius To Check For Player Bases Blocking Egress")]
55 55 public float BaseBlockRadius = 100f;
56 56
57 57 [JsonProperty("Retry Delay When Egress Blocked By A Player Base (Seconds)")]
58 58 public float BaseBlockRetryDelay = 60f;
59 59
60 60 [JsonProperty("Delay Before Explosions Start After Egress Is Triggered (Seconds)")]
61 61 public int ExplodeDelay = 80;
62 62
63 63 [JsonProperty("Delay Between Each Explosion (Seconds)")]
64 64 public float DelayBetweenExplosion = 1.0f;
65 65
66 66 [JsonProperty("Sink Speed (Negative = Downward, Units Per Second)")]
67 67 public float SinkSpeed = -0.85f;
68 68
69 69 [JsonProperty("Height (Y) At Which The Ship Is Considered Fully Sunk")]
70 70 public int SinkAmount = -45;
71 71
72 72 [JsonProperty("Give Radiation To Nearby Players While Sinking")]
73 73 public bool GiveRadsOnSink = true;
74 74
75 75 [JsonProperty("Delay After Fully Sunk Before The Ship Entity Is Removed (Seconds)")]
76 76 public int SunkDeathDelay = 80;
77 77
78 78 [JsonProperty("Radius To Check For Nearby Players Before Removing A Sunk Ship")]
79 79 public float KillCheckRadius = 100f;
80 80
81 81 [JsonProperty("Retry Delay When A Nearby Player Blocks Ship Removal (Seconds)")]
82 82 public float KillRetryDelay = 60f;
83 83
84 84 [JsonProperty("Harbor Pathing: Terrain Height That Blocks A Path (Meters)")]
85 85 public float HarborPathHeightBlock = 3f;
86 86
87 87 [JsonProperty("Harbor Pathing: Step Size Used To Scan For Blocked Paths (Meters)")]
88 88 public float HarborPathStepSize = 2f;
89 89
90 90 [JsonProperty("Harbor Pathing: Distance Penalty Multiplier When No Line Of Sight")]
91 91 public float HarborPathNoLosPenalty = 20f;
92 92
93 93 [JsonProperty("Harbor Pathing: Line Of Sight Layer Mask")]
94 94 public int HarborPathLayerMask = 1084293377;
95 95
96 96 public string ToJson() => JsonConvert.SerializeObject(this);
97 97 public Dictionary<string, object> ToDictionary() => JsonConvert.DeserializeObject<Dictionary<string, object>>(ToJson());
98 98 }
99 99
100 100 protected override void LoadDefaultConfig()
101 101 {
102 102 config = new Configuration();
103 103 }
104 104 protected override void LoadConfig()
105 105 {
106 106 base.LoadConfig();
107 107 try
108 108 {
109 109 config = Config.ReadObject<Configuration>();
110 110 if (config == null)
111 111 {
112 112 throw new JsonException();
113 113 }
114 114
115 115 if (!config.ToDictionary().Keys.SequenceEqual(Config.ToDictionary(x => x.Key, x => x.Value).Keys))
116 116 {
117 117 PrintWarning("Configuration appears to be outdated; updating and saving");
118 118 SaveConfig();
119 119 }
120 120 }
121 121 catch
122 122 {
123 123 PrintWarning($"Configuration file {Name}.json is invalid; using defaults");
124 124 LoadDefaultConfig();
125 125 }
126 126 }
127 127 protected override void SaveConfig()
128 128 {
129 129 PrintWarning($"Configuration changes saved to {Name}.json");
130 130 Config.WriteObject(config, true);
131 131 }
132 132 #endregion Configuration
133 133
134 134 private void SinkMeCommand(BasePlayer player, string command, string[] args)
135 135 {
136 136 bool allowed = config.RequirePermission
137 137 ? permission.UserHasPermission(player.UserIDString, config.SinkPermission)
138 138 : player.IsAdmin;
139 139 if (!allowed) { player.ChatMessage("You do not have permission to use this command."); return; }
140 140
141 141 RaycastHit hit;
142 142 var raycast = Physics.Raycast(player.eyes.HeadRay(), out hit, 50, -1);
143 143 BaseEntity entity = raycast ? hit.GetEntity() : null;
144 144 if (entity != null && entity is CargoShip) { (entity as CargoShip)?.StartEgress(); }
145 145 }
146 146
147 147 private void OnServerInitialized()
148 148 {
149 149 CargoShip.hasCalculatedApproaches = false;
150 150 if (config.ReverseCargoPath)
151 151 {
152 152 TerrainMeta.Path.OceanPatrolFar.Reverse(); //Reverse cargopath
153 153 }
154 154 }
155 155
156 156 private void Init()
157 157 {
158 158 cargoSinks = this;
159 159 permission.RegisterPermission(config.SinkPermission, this);
160 160 cmd.AddChatCommand(config.SinkCommand, this, nameof(SinkMeCommand));
161 161 }
162 162
163 163 private void Unload()
164 164 {
165 165 cargoSinks = null;
166 166 if (config.ReverseCargoPath)
167 167 {
168 168 TerrainMeta.Path.OceanPatrolFar.Reverse(); //Restore cargopath
169 169 }
170 170 foreach (var c in CargoKillShipList) { if (c != null) { c.Kill(); } }
171 171 }
172 172
173 173 private void OnEntitySpawned(CargoShip cs)
174 174 {
175 175 if (cs != null)
176 176 {
177 177 Vector3 pos = TerrainMeta.Path.OceanPatrolFar.GetRandom();
178 178 pos.y = 0;
179 179 cs.transform.position = pos;
180 180 int index = GetClosestNodeToUs(pos) - 1;
181 181 if (index < 0) { index = TerrainMeta.Path.OceanPatrolFar.Count - 1; }
182 182 pos = TerrainMeta.Path.OceanPatrolFar[index];
183 183 pos.y = 0;
184 184 cs.transform.LookAt(pos);
185 185 cs.SendNetworkUpdateImmediate();
186 186 if (config.Debug) Puts("Corrected Cargo Position");
187 187 timer.Once(5f, () =>
188 188 {
189 189 if (cs != null)
190 190 {
191 191 int Entities = 2;
192 192 foreach (var pe in cs.children)
193 193 {
194 194 if (pe != null)
195 195 {
196 196 try
197 197 {
198 198 if (pe is RHIB) { pe.transform.localPosition = SubEntities[0]; }
199 199 else if (pe is MicrophoneStand) { pe.transform.localPosition = SubEntities[1]; }
200 200 else if (pe is ConnectedSpeaker) { pe.transform.localPosition = SubEntities[Entities++]; }
201 201 }
202 202 catch { }
203 203 }
204 204 }
205 205 if (config.Debug) Puts("Corrected SubEntities Positions");
206 206 }
207 207 });
208 208 }
209 209 }
210 210
211 211 public int GetClosestNodeToUs(Vector3 position)
212 212 {
213 213 int result = 0;
214 214 try
215 215 {
216 216 float num = float.PositiveInfinity;
217 217 for (int i = 0; i < TerrainMeta.Path.OceanPatrolFar.Count; i++)
218 218 {
219 219 Vector3 b = TerrainMeta.Path.OceanPatrolFar[i];
220 220 float num2 = Vector3.Distance(position, b);
221 221 if (num2 < num)
222 222 {
223 223 result = i;
224 224 num = num2;
225 225 }
226 226 }
227 227 }
228 228 catch { }
229 229 return result;
230 230 }
231 231
232 232 private object OnCargoShipEgress(CargoShip _cargoship)
233 233 {
234 234 if (_cargoship != null)
235 235 {
236 236 //Prevent leave when in topology
237 237 if (TerrainMeta.TopologyMap.GetTopology(_cargoship.transform.position, config.TopologyBaseBlock))
238 238 {
239 239 timer.Once(config.TopologyBlockRetryDelay, () => { if (_cargoship != null) { _cargoship.StartEgress(); } });
240 240 if (config.Debug) Puts("Topo Blocked");
241 241 return true;
242 242 }
243 243 //Prevent leave when near player base
244 244 List<BuildingBlock> list = Facepunch.Pool.Get<List<BuildingBlock>>();
245 245 Vis.Entities<BuildingBlock>(_cargoship.transform.position, config.BaseBlockRadius, list);
246 246 if (list.Count != 0)
247 247 {
248 248 timer.Once(config.BaseBlockRetryDelay, () => { if (_cargoship != null) { _cargoship.StartEgress(); } });
249 249 Facepunch.Pool.FreeUnmanaged<BuildingBlock>(ref list);
250 250 if (config.Debug) Puts("Base Blocked");
251 251 return true;
252 252 }
253 253 Facepunch.Pool.FreeUnmanaged<BuildingBlock>(ref list);
254 254 if (config.Debug) Puts("Start Egress");
255 255 Vector3 bow = _cargoship.transform.position + (_cargoship.transform.forward * 81) + (_cargoship.transform.up * 1);
256 - _cargoship.SetFlag(BaseEntity.Flags.Reserved8, true, false, true);
256 + _cargoship.SetFlagLocal(BaseEntity.Flags.Reserved8, true, false);
257 257 _cargoship.targetNodeIndex = -1;
258 258 _cargoship.currentThrottle = 0;
259 + _cargoship.SendNetworkUpdateImmediate();
259 260 List<Vector3> ExpoPos = new List<Vector3>();
260 261 for (int i = 0; i < 9; i++)
261 262 {
262 263 ExpoPos.Add(bow + (_cargoship.transform.forward * -32) + (_cargoship.transform.forward * (-10 * i)) + (_cargoship.transform.right * 6));
263 264 ExpoPos.Add(bow + (_cargoship.transform.forward * -32) + (_cargoship.transform.forward * (-10 * i)) + (_cargoship.transform.right * -6));
264 265 }
265 266 RunEffect(c4, null, bow + (_cargoship.transform.forward * -7) + (_cargoship.transform.right * 2));
266 267 RunEffect(debris, null, bow + (_cargoship.transform.forward * -7) + (_cargoship.transform.right * 2));
267 268 RunEffect(c4, null, bow + (_cargoship.transform.forward * -7) + (_cargoship.transform.right * -2));
268 269 RunEffect(debris, null, bow + (_cargoship.transform.forward * -7) + (_cargoship.transform.right * -2));
269 270 timer.Once(config.ExplodeDelay, () => { if (_cargoship != null) { playexp(ExpoPos, _cargoship); } });
270 271 CargoKillShipList.Add(_cargoship);
271 272 }
272 273 return null;
273 274 }
274 275
275 276 private void RunEffect(string name, BaseEntity entity = null, Vector3 position = new Vector3(), Vector3 offset = new Vector3())
276 277 {
277 278 if (entity != null) { Effect.server.Run(name, entity, 0, offset, position, null, true); return; }
278 279 Effect.server.Run(name, position, Vector3.up, null, true);
279 280 }
280 281
281 282 private void playexp(List<Vector3> ExPoint, CargoShip cargoShip)
282 283 {
283 284 if (ExPoint != null && ExPoint.Count > 1)
284 285 {
285 286 RunEffect(c4, null, ExPoint[0]);
286 287 RunEffect(debris, null, ExPoint[1]);
287 288 ExPoint.RemoveAt(0);
288 289 ExPoint.RemoveAt(0);
289 290 timer.Once(config.DelayBetweenExplosion, () => { playexp(ExPoint, cargoShip); });
290 291 return;
291 292 }
292 293 if (config.Debug) Puts("Expo Ran");
293 294 for (int i = cargoShip.children.Count - 1; i >= 0; i--)
294 295 {
295 296 BaseEntity baseEntity = cargoShip.children[i];
296 297 if (baseEntity != null && !baseEntity.IsDestroyed && baseEntity is ScientistNPC)
297 298 {
298 299 if ((baseEntity as ScientistNPC).IsAlive()) { baseEntity.Kill(); }
299 300 }
300 301 }
301 302 if (config.Debug) Puts("NPCs Killed");
302 303 Sink(cargoShip);
303 304 }
304 305
305 306 private void Sink(CargoShip _cargoship)
306 307 {
307 308 if (_cargoship != null)
308 309 {
309 310 _cargoship.transform.position += _cargoship.transform.up * config.SinkSpeed * Time.deltaTime;
310 311 if (_cargoship.transform.position.y <= config.SinkAmount)
311 312 {
312 313 if (config.GiveRadsOnSink) { _cargoship.InvokeRepeating(new Action(_cargoship.UpdateRadiation), 10f, 5f); }
313 314 timer.Once(config.SunkDeathDelay, () => { killme(_cargoship); });
314 315 }
315 316 else { _cargoship.Invoke(() => { Sink(_cargoship); }, 0.02f); }
316 317 }
317 318 }
318 319
319 320 public void killme(CargoShip _cargoship)
320 321 {
321 322 if (_cargoship == null) { return; }
322 323 if (BaseNetworkable.HasCloseConnections(_cargoship.transform.position, config.KillCheckRadius))
323 324 {
324 325 timer.Once(config.KillRetryDelay, () => { killme(_cargoship); });
325 326 if (config.Debug) Puts("Blocked Kill Player Nearby");
326 327 return;
327 328 }
328 329 foreach (BaseEntity b in _cargoship.children.ToArray())
329 330 {
330 331 if (b != null && b.GetParentEntity() is CargoShip)
331 332 {
332 333 if (b is BasePlayer || b is LootableCorpse || b is BaseCorpse || b is PlayerCorpse)
333 334 {
334 335 Vector3 oldpos = b.transform.position;
335 336 oldpos.y = 1;
336 337 b.SetParent(null, true, true);
337 338 if (config.Debug) Puts("Move Loot");
338 339 continue;
339 340 }
340 341 b.Kill();
341 342 }
342 343 }
343 344 NextFrame(() =>
344 345 {
345 346 CargoKillShipList.Remove(_cargoship);
346 347 _cargoship.Kill();
347 348 if (config.Debug) Puts("End");
348 349 });
349 350 }
350 351
351 352 [AutoPatch]
352 353 [HarmonyPatch(typeof(CargoShip), "CalculateHarborApproachNodes")]
353 354 public static class CargoShip_CalculateHarborApproachNodes_Patch
354 355 {
355 356 [HarmonyPrefix]
356 357 public static bool Prefix()
357 358 {
358 359 if (CargoShip.hasCalculatedApproaches) return false;
359 360
360 361 CargoShip.hasCalculatedApproaches = true;
361 362 Configuration cfg = cargoSinks.config;
362 363
363 364 for (int i = 0; i < CargoShip.harbors.Count; i++)
364 365 {
365 366 CargoShip.HarborInfo harborInfo = CargoShip.harbors[i];
366 367 cargoSinks.Puts($"Harbor {i} is generating path!");
367 368 float bestDistance = float.MaxValue;
368 369 int bestNodeIndex = -1;
369 370
370 371 Vector3 harborPos = harborInfo.harborPath.nodes[0].Position;
371 372
372 373 for (int j = 0; j < TerrainMeta.Path.OceanPatrolFar.Count; j++)
373 374 {
374 375 Vector3 nodePos = TerrainMeta.Path.OceanPatrolFar[j];
375 376 float distance = Vector3.Distance(nodePos, harborPos);
376 377 bool pathBlockedByLand = false;
377 378 for (float step = 0; step < distance; step += cfg.HarborPathStepSize)
378 379 {
379 380 Vector3 checkPoint = Vector3.MoveTowards(nodePos, harborPos, step);
380 381 float terrainHeight = TerrainMeta.HeightMap.GetHeight(checkPoint);
381 382 if (terrainHeight > cfg.HarborPathHeightBlock)
382 383 {
383 384 pathBlockedByLand = true;
384 385 break;
385 386 }
386 387 }
387 388
388 389 if (pathBlockedByLand) continue;
389 390
390 391 Vector3 heightOffset = Vector3.up * 3f;
391 392 if (!GamePhysics.LineOfSightRadius(nodePos + heightOffset, harborPos + heightOffset, cfg.HarborPathLayerMask, 3f, null))
392 393 {
393 394 distance *= cfg.HarborPathNoLosPenalty;
394 395 }
395 396
396 397 if (distance < bestDistance)
397 398 {
398 399 bestDistance = distance;
399 400 bestNodeIndex = j;
400 401 }
401 402 }
402 403
403 404 if (bestNodeIndex == -1)
404 405 {
405 406 cargoSinks.Puts($"[CargoPatch] Harbor {i} is isolated! No ocean node has a clear water path to it.");
406 407 continue;
407 408 }
408 409
409 410 harborInfo.approachNode = bestNodeIndex;
410 411 CargoShip.harbors[i] = harborInfo;
411 412 }
412 413
413 414 return false;
414 415 }
415 416 }
416 417 }
417 418 }
418 419