← Back to changelog

What changed

CH47SpawnPoint · v1.2.6 → v1.2.7 (current)

Diff 9 added · 9 removed · 902 → 902 total lines
1 1 /*▄▄▄ ███▄ ▄███▓ ▄████ ▄▄▄██▀▀▀▓█████▄▄▄█████▓
2 2 ▓█████▄ ▓██▒▀█▀ ██▒ ██▒ ▀█▒ ▒██ ▓█ ▀▓ ██▒ ▓▒
3 3 ▒██▒ ▄██▓██ ▓██░▒██░▄▄▄░ ░██ ▒███ ▒ ▓██░ ▒░
4 4 ▒██░█▀ ▒██ ▒██ ░▓█ ██▓▓██▄██▓ ▒▓█ ▄░ ▓██▓ ░
5 5 ░▓█ ▀█▓▒██▒ ░██▒░▒▓███▀▒ ▓███▒ ░▒████▒ ▒██▒ ░
6 6 ░▒▓███▀▒░ ▒░ ░ ░ ░▒ ▒ ▒▓▒▒░ ░░ ▒░ ░ ▒ ░░
7 7 ▒░▒ ░ ░ ░ ░ ░ ░ ▒ ░▒░ ░ ░ ░ ░
8 8 ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░
9 9 ░ ░ ░ ░ ░ ░ ░
10 10 Permissions:
11 11 CH47SpawnPoint.Spawn - Allows players to bag on heli
12 12 CH47SpawnPoint.Mount - Mounts players to heli
13 13 CH47SpawnPoint.Drop - Allows player to use /dropme chat command to TP to drop heli
14 14 */
15 15 using Facepunch;
16 16 using HarmonyLib;
17 17 using Newtonsoft.Json;
18 18 using Oxide.Core.Plugins;
19 19 using Rust;
20 20 using System;
21 21 using System.Collections.Generic;
22 22 using System.Linq;
23 23 using UnityEngine;
24 24 using Time = UnityEngine.Time;
25 25
26 26 namespace Oxide.Plugins
27 27 {
28 - [Info("CH47SpawnPoint", "bmgjet", "1.2.6")]
28 + [Info("CH47SpawnPoint", "bmgjet", "1.2.7")]
29 29 [Description("Creates a CH47 that is a roaming spawn point.")]
30 30 class CH47SpawnPoint : RustPlugin
31 31 {
32 32 [PluginReference]
33 33 private Plugin ServerRewards, Economics;
34 34
35 35 //Variables
36 36 public static CH47SpawnPoint plugin;
37 37 private bool _protectHeli = true;
38 38 private CH47HelicopterAIController SpawnedCH47;
39 39 private Vector3 oldPos = Vector3.zero;
40 40 private CH47AIBrain Brain;
41 41 private List<SleepingBag> SpawnedBag = new List<SleepingBag>();
42 42 private List<BaseMountable> ExtraSeats = new List<BaseMountable>();
43 43 private Dictionary<ulong, Parachute> ParachutingPlayers = new Dictionary<ulong, Parachute>();
44 44 private Timer SpawnedTimer;
45 45 private Dictionary<ulong, float> CombatBlocked = new Dictionary<ulong, float>(); //Combat Block Timer
46 46 private const string CH45Prefab = "assets/prefabs/npc/ch47/ch47scientists.entity.prefab";
47 47
48 48 #region Language
49 49 protected override void LoadDefaultMessages()
50 50 {
51 51 lang.RegisterMessages(new Dictionary<string, string>
52 52 {
53 53 {"Mounted", "Can't use while you are mounted."},
54 54 {"BuildingBlocked", "Can't use while building blocked."},
55 55 {"InsideBlocked", "Can't use while inside a building."},
56 56 {"SafezoneBlocked", "You must be in a safezone to use this command."},
57 57 {"UndergroundBlocked", "Can't use while Underground."},
58 58 {"HostileBlocked", "Can't use while hostile. ({0})"},
59 59 {"WoundedBlocked", "Can't use while wounded."},
60 60 {"CombatBlocked", "Can't use while combat blocked ({0})"},
61 61 {"CantAfford", "Looks like you can not afford to buy this" },
62 62 {"Charged", "Charged {0} {1} for CH47 Drop" },
63 63 {"UnsupportedCurrency", "Currency Type Not supported on this server" },
64 64 }, this);
65 65 }
66 66
67 67 //Language lookups
68 68 private string message(BasePlayer player, string key, params object[] args)
69 69 {
70 70 if (player == null) { return string.Format(lang.GetMessage(key, this), args); }
71 71 return string.Format(lang.GetMessage(key, this, player.UserIDString), args);
72 72 }
73 73 #endregion
74 74
75 75 #region Configuration
76 76 private Configuration config;
77 77 private class Configuration
78 78 {
79 79 [JsonProperty("Give Parachute Item And Equip It To Player")]
80 80 public bool GiveParachute = true;
81 81 [JsonProperty("Single Use Parachute")]
82 82 public bool SingleUseChute = true;
83 83 [JsonProperty("Force Other Plugins Parachutes To Single Use")]
84 84 public bool ForceSingleUseChute = false;
85 85 [JsonProperty("Force Remove Players Parachute On Dismount When Jumped From CH47 (Likely To Break Other Plugins)")]
86 86 public bool ForceRemove = false;
87 87 [JsonProperty("Auto Deploy Parachute")]
88 88 public bool AutoDeploy = true;
89 89 [JsonProperty("Auto Deploy Height Above Terrain (Min Height 30)")]
90 90 public int AutoDeployHeight = 80;
91 91 [JsonProperty("Auto Deploy Check Tick (20ms X this value)")]
92 92 public int AutoDeployTick = 10;
93 93 [JsonProperty("CH47 Remove Map Marker")]
94 94 public bool RemoveMapMarker = true;
95 95 [JsonProperty("CH47 Min Hover Target Height")]
96 96 public float HoverHeight = 300;
97 97 [JsonProperty("CH47 Max Hover Target Height")]
98 98 public float MaxHoverHeight = 520;
99 99 [JsonProperty("CH47 Movement Steps ((MapSize / 2) * ThisValue)")]
100 100 public float ScanSize = 0.85f;
101 101 [JsonProperty("CH47 Max Distance From Center (-1 = disabled)")]
102 102 public float MaxDistance = -1f;
103 103 [JsonProperty("CH47 Tick (Updates Movement Direction And Checks CH47 Alive)")]
104 104 public float HeliTick = 60;
105 105 [JsonProperty("Spawn Bag Text")]
106 106 public string BagText = "Heli Drop";
107 107 [JsonProperty("Spawn Unlock Delay (Sec)")]
108 108 public int BagDelay = 60;
109 109 [JsonProperty("Make Heli Safe Zone (High Performance Hit)")]
110 110 public bool SafeZoneHeli = false;
111 111 [JsonProperty("Mount Free Heli Seats First")]
112 112 public bool UseNativeSeatsFirst = true;
113 113 [JsonProperty("Spawn Point TimeOut")]
114 114 public float SpawnTimeOut = 120;
115 115 [JsonProperty("FlyHack Disable Distance Around Heli")]
116 116 public float FlyHackDistance = 50;
117 117 [JsonProperty("Parachute SkinID")]
118 118 public ulong SkinID = 0;
119 119 [JsonProperty("Parachute Disable Collision Hurt")]
120 120 public bool DisableCollisionHurt = true;
121 121 [JsonProperty("Parachute HurtAmount")]
122 122 public float HurtAmount = 80f;
123 123 [JsonProperty("Parachute UprightLerpForce")]
124 124 public float UprightLerpForce = 25f;
125 125 [JsonProperty("Parachute ConstantForwardForce")]
126 126 public float ConstantForwardForce = 35f;
127 127 [JsonProperty("Parachute TurnForce")]
128 128 public float TurnForce = 5f;
129 129 [JsonProperty("Parachute ForwardTiltAcceleration")]
130 130 public float ForwardTiltAcceleration = 40f;
131 131 [JsonProperty("Parachute BackInputForceMultiplier")]
132 132 public float BackInputForceMultiplier = 0.6f;
133 133 [JsonProperty("Parachute TargetDrag")]
134 134 public float TargetDrag = 5f;
135 135 [JsonProperty("Parachute TargetAngularDrag")]
136 136 public float TargetAngularDrag = 1.5f;
137 137 [JsonProperty("SamSites Ignore Spawn Parachute")]
138 138 public bool IgnoreSpawnChute = false;
139 139 [JsonProperty("Block DropMe While Building Blocked")]
140 140 public bool Disablebuildingblocked = true;
141 141 [JsonProperty("Block DropMe While Marked As Hostile")]
142 142 public bool Disablehostile = true;
143 143 [JsonProperty("Block DropMe While Wounded")]
144 144 public bool Disablewounded = true;
145 145 [JsonProperty("Block DropMe While Indoors")]
146 146 public bool DisableIndoors = true;
147 147 [JsonProperty("Block DropMe While Underground")]
148 148 public bool DisableUnderground = true;
149 149 [JsonProperty("Allow DropMe Only At SafeZones")]
150 150 public bool SafezoneOnlyDropme = true;
151 151 [JsonProperty("Disable DropMe For X Sec After Taking Damage (0 To Disable)")]
152 152 public int Disabledamage = 0;
153 153 [JsonProperty("Min Damage To Consider As Taking Damage")]
154 154 public int MinDamage = 5;
155 155 [JsonProperty("Cost Type (free, economics, serverrewards)")]
156 156 public string Costtype = "free";
157 157 [JsonProperty("Cost Amount")]
158 158 public int Costamount = 100;
159 159 [JsonProperty("Currency Symbol for (serverrewards,economics)")]
160 160 public string CurrencySymbol = "$";
161 161
162 162 public string ToJson() => JsonConvert.SerializeObject(this);
163 163 public Dictionary<string, object> ToDictionary() => JsonConvert.DeserializeObject<Dictionary<string, object>>(ToJson());
164 164 }
165 165
166 166 protected override void LoadDefaultConfig() { config = new Configuration(); }
167 167 protected override void LoadConfig()
168 168 {
169 169 base.LoadConfig();
170 170 try
171 171 {
172 172 config = Config.ReadObject<Configuration>();
173 173 if (config == null) { throw new JsonException(); }
174 174
175 175 if (!config.ToDictionary().Keys.SequenceEqual(Config.ToDictionary(x => x.Key, x => x.Value).Keys))
176 176 {
177 177 PrintWarning("Configuration appears to be outdated; updating and saving");
178 178 SaveConfig();
179 179 }
180 180 }
181 181 catch
182 182 {
183 183 PrintWarning($"Configuration file {Name}.json is invalid; using defaults");
184 184 LoadDefaultConfig();
185 185 }
186 186 }
187 187 protected override void SaveConfig()
188 188 {
189 189 PrintWarning($"Configuration changes saved to {Name}.json");
190 190 Config.WriteObject(config, true);
191 191 }
192 192 #endregion Configuration
193 193
194 194 #region Oxide Hooks
195 195 private void Init()
196 196 {
197 197 plugin = this;
198 198 permission.RegisterPermission("CH47SpawnPoint.Spawn", this);
199 199 permission.RegisterPermission("CH47SpawnPoint.Mount", this);
200 200 permission.RegisterPermission("CH47SpawnPoint.Drop", this);
201 201 permission.RegisterPermission("CH47SpawnPoint.Force", this);
202 202 Unsubscribe(nameof(OnEntityKill));
203 203 }
204 204
205 205 [ChatCommand("admindrop")]
206 206 private void AdminHeliDrop(BasePlayer player, string command, string[] args)
207 207 {
208 208 if (player.IsAdmin) //limit to admin
209 209 {
210 210 foreach (BasePlayer aplayer in BasePlayer.activePlayerList)
211 211 {
212 212 if (!aplayer.IsNpc && player.IsAlive()) { DropPlayerFromHeli(aplayer); }
213 213 }
214 214 }
215 215 }
216 216
217 217 [ChatCommand("dropme")]
218 218 private void PlayerHeliDrop(BasePlayer player, string command, string[] args)
219 219 {
220 220 if (permission.UserHasPermission(player.UserIDString, "CH47SpawnPoint.Drop") && IsAllowed(player)) //limit to permission and conditions
221 221 {
222 222 DropPlayerFromHeli(player);
223 223 }
224 224 }
225 225 private object OnEntityKill(CH47Helicopter entity)
226 226 {
227 227 if (_protectHeli == true && SpawnedCH47 != null && entity?.net?.ID != null && SpawnedCH47.net.ID.Value == entity.net.ID.Value)
228 228 {
229 229 return true;
230 230 }
231 231 return null;
232 232 }
233 233 private void OnServerInitialized(bool initial) { timer.Once(initial ? 10f : 3f, () => { Subscribe(nameof(OnEntityKill)); CreateSpawnPointHeli(); }); }
234 234 private void OnPlayerRespawned(BasePlayer player) { CheckSpawn(player); }
235 235 private void OnPlayerSleepEnded(BasePlayer player) { if (player.IsNpc) { return; } WakeCheck(player); }
236 236 private void OnPlayerConnected(BasePlayer player) { MakeBag(player); }
237 237 private void OnPlayerDeath(BasePlayer player) { if (ParachutingPlayers.ContainsKey(player.userID)) { ParachutingPlayers.Remove(player.userID); } MakeBag(player); }
238 238 private void OnEntityDismounted(BaseMountable entity, BasePlayer player)
239 239 {
240 240 if (entity == null) { return; }
241 241 bool wasExtraSeat = ExtraSeats.Contains(entity);
242 242 if (wasExtraSeat) { ExtraSeats.Remove(entity); if (!entity.IsDestroyed) { entity.Kill(); } }
243 243 //Clear the SafeZone flag SafeZoneTrigger sets while mounted, so it doesn't stick permanently after leaving the heli
244 244 if (config.SafeZoneHeli && player != null && (wasExtraSeat || (SpawnedCH47 != null && entity.GetParentEntity() == SpawnedCH47)))
245 245 {
246 246 player.SetPlayerFlag(BasePlayer.PlayerFlags.SafeZone, false);
247 247 }
248 248 }
249 249 private void OnPlayerDisconnected(BasePlayer player) { if (player == null) { return; } if (ParachutingPlayers.ContainsKey(player.userID)) { ParachutingPlayers.Remove(player.userID); } if (player.GetParentEntity() == SpawnedCH47) { player.SetParent(null, true, true); } CleanBags(player); }
250 250 private object OnPlayerViolation(BasePlayer player, AntiHackType type)
251 251 {
252 252 if (player != null && SpawnedCH47 != null && type == AntiHackType.FlyHack && Vector3.Distance(player.transform.position, SpawnedCH47.transform.position) < config.FlyHackDistance) { return true; }
253 253 return null;
254 254 }
255 255 private object OnEntityTakeDamage(BaseCombatEntity entity, HitInfo info)
256 256 {
257 257 if (info?.Initiator == null || SpawnedCH47 == null) { return null; }
258 258 try
259 259 {
260 260 if (config.Disabledamage != 0 && entity.ToPlayer() != null)
261 261 {
262 262 if (!entity.ToPlayer().IsImmortalTo(info) && info.damageTypes.Total() > config.MinDamage)
263 263 {
264 264 BasePlayer damagedPlayer = entity.ToPlayer();
265 265 CombatBlocked[damagedPlayer.userID] = Time.time;
266 266 }
267 267 }
268 268 if (info.Initiator == SpawnedCH47 || entity == SpawnedCH47) { return true; }
269 269 if (config.SafeZoneHeli && Vector3.Distance(entity.transform.position, SpawnedCH47.transform.position) <= 15 && info.damageTypes.GetMajorityDamageType() != DamageType.Suicide) { return true; }
270 270 }
271 271 catch { }
272 272 return null;
273 273 }
274 274
275 275 private void Unload() { UnloadPlugin(); plugin = null; }
276 276 #endregion
277 277
278 278 #region Methods
279 279 private void UnloadPlugin()
280 280 {
281 281 _protectHeli = false;
282 282 if (Brain != null) { Brain = null; }
283 283 if (SpawnedTimer != null) { SpawnedTimer.Destroy(); }
284 284 if (SpawnedBag != null) { foreach (var bag in SpawnedBag) { if (!bag.IsDestroyed) { bag.Kill(); } } SpawnedBag.Clear(); }
285 285 if (ExtraSeats != null) { foreach (var mount in ExtraSeats) { if (!mount.IsDestroyed) { mount.Kill(); } } ExtraSeats.Clear(); }
286 286 if (SpawnedCH47 != null) { for (int i = SpawnedCH47.children.Count - 1; i >= 0; i--) { SpawnedCH47.children[i].Kill(); } SpawnedCH47.Kill(); SpawnedCH47 = null; }
287 287 if (config.AutoDeploy)
288 288 {
289 289 foreach (var player in BasePlayer.activePlayerList)
290 290 {
291 291 try
292 292 {
293 293 AutoChuteChcker acc = player.gameObject.GetComponent<AutoChuteChcker>();
294 294 if (acc != null) { acc.RemoveMe(); }
295 295 }
296 296 catch { }
297 297 }
298 298 }
299 299 }
300 300
301 301 public bool IsAllowed(BasePlayer player)
302 302 {
303 303 //Check conditions of use
304 304 if (player.isMounted)
305 305 {
306 306 player.ChatMessage(message(player, "Mounted"));
307 307 return false;
308 308 }
309 309 if (config.SafezoneOnlyDropme && player.InSafeZone())
310 310 {
311 311 return true;
312 312 }
313 313 else if (config.SafezoneOnlyDropme && !player.InSafeZone())
314 314 {
315 315 player.ChatMessage(message(player, "SafezoneBlocked"));
316 316 return false;
317 317 }
318 318 if (config.DisableUnderground && TerrainMeta.HeightMap.GetHeight(player.transform.position) < -1)
319 319 {
320 320 player.ChatMessage(message(player, "UndergroundBlocked"));
321 321 return false;
322 322 }
323 323 if (config.DisableIndoors && !player.IsOutside())
324 324 {
325 325 player.ChatMessage(message(player, "InsideBlocked"));
326 326 return false;
327 327 }
328 328 if (player.IsBuildingBlocked() && config.Disablebuildingblocked)
329 329 {
330 330 player.ChatMessage(message(player, "BuildingBlocked"));
331 331 return false;
332 332 }
333 333 if (player.IsHostile() && config.Disablehostile)
334 334 {
335 335 player.ChatMessage(message(player, "HostileBlocked", player.GetHostileDuration().ToString("n2")));
336 336 return false;
337 337 }
338 338 if (player.IsWounded() && config.Disablewounded)
339 339 {
340 340 player.ChatMessage(message(player, "WoundedBlocked"));
341 341 return false;
342 342 }
343 343 if (CombatBlocked.ContainsKey(player.userID) && CombatBlocked[player.userID] + config.Disabledamage > Time.time)
344 344 {
345 345 player.ChatMessage(message(player, "CombatBlocked", ((CombatBlocked[player.userID] + config.Disabledamage) - Time.time).ToString("n2")));
346 346 return false;
347 347 }
348 348 CombatBlocked.Remove(player.userID);
349 349 return ChargePlayer(player);
350 350 }
351 351
352 352 bool ChargePlayer(BasePlayer player)
353 353 {
354 354 object result = null;
355 - if (config.Costtype == "free"){return true;}
355 + if (config.Costtype == "free") { return true; }
356 356 else
357 357 {
358 358 if (config.Costtype == "serverrewards" && ServerRewards != null)
359 359 {
360 360 result = ServerRewards.Call("TakePoints", player.UserIDString, (int)config.Costamount);
361 361 }
362 362 else if (config.Costtype == "economics" && Economics != null)
363 363 {
364 364 result = Economics.Call("Withdraw", player.UserIDString, (double)config.Costamount);
365 365 }
366 366 else
367 367 {
368 - player.ChatMessage( message(player, "UnsupportedCurrency"));
368 + player.ChatMessage(message(player, "UnsupportedCurrency"));
369 369 return false;
370 370 }
371 371 if (result == null || (result is bool && (bool)result == false))
372 372 {
373 - player.ChatMessage( message(player, "CantAfford"));
373 + player.ChatMessage(message(player, "CantAfford"));
374 374 return false;
375 375 }
376 376 player.ChatMessage(message(player, "Charged", config.Costamount, config.CurrencySymbol));
377 377 return true;
378 378 }
379 379 }
380 380
381 381 private void DropPlayerFromHeli(BasePlayer player)
382 382 {
383 383 if (player == null || SpawnedCH47 == null) { return; }
384 384 player.StartSleeping();
385 385 player.Teleport(SpawnedCH47.transform.position + SpawnedCH47.transform.up * 1 + SpawnedCH47.transform.forward * -6.25f);
386 386 player.SetParent(SpawnedCH47, true, true);
387 387 }
388 388
389 389 private void MakeBag(BasePlayer player)
390 390 {
391 391 if (player == null || SpawnedBag == null || SpawnedCH47 == null || !permission.UserHasPermission(player.UserIDString, "CH47SpawnPoint.Spawn")) { return; }
392 392 foreach (var bag in SpawnedBag) { if (bag.deployerUserID == player.userID) { return; } }
393 393 SleepingBag sleepingbag = (SleepingBag)GameManager.server.CreateEntity("assets/prefabs/deployable/sleeping bag/sleepingbag_leather_deployed.prefab", SpawnedCH47.transform.position + SpawnedCH47.transform.up * 1.25f + SpawnedCH47.transform.forward * 5.25f);
394 394 if (sleepingbag == null)
395 395 {
396 396 PrintError("Failed to create spawn point sleeping bag entity.");
397 397 return;
398 398 }
399 399 sleepingbag.health = sleepingbag.MaxHealth();
400 400 sleepingbag.deployerUserID = player.userID;
401 401 sleepingbag.niceName = config.BagText;
402 402 sleepingbag.EnableSaving(false);
403 403 sleepingbag.Spawn();
404 404 foreach (var mesh in sleepingbag.GetComponentsInChildren<MeshCollider>()) { UnityEngine.Object.DestroyImmediate(mesh); }
405 405 sleepingbag.SetUnlockTime(UnityEngine.Time.realtimeSinceStartup + config.BagDelay);
406 406 sleepingbag.SetParent(SpawnedCH47, true, true);
407 407 SpawnedBag.Add(sleepingbag);
408 408 //Time Out Remove Bag
409 409 timer.Once(config.BagDelay + config.SpawnTimeOut, () =>
410 410 {
411 411 if (sleepingbag == null || SpawnedBag == null || !SpawnedBag.Contains(sleepingbag)) { return; }
412 412 if (!sleepingbag.IsDestroyed) { SpawnedBag.Remove(sleepingbag); sleepingbag.Kill(); }
413 413 });
414 414 }
415 415
416 416 private void CleanBags(BasePlayer player)
417 417 {
418 418 for (int i = SpawnedBag.Count - 1; i >= 0; i--)
419 419 {
420 420 if (SpawnedBag[i] == null)
421 421 {
422 422 SpawnedBag.RemoveAt(i);
423 423 continue;
424 424 }
425 425 BasePlayer scanplayer = BasePlayer.FindByID(SpawnedBag[i].deployerUserID);
426 426 if (scanplayer != null && scanplayer.IsAlive() && !SpawnedBag[i].IsDestroyed)
427 427 {
428 428 SpawnedBag[i].Kill();
429 429 SpawnedBag.RemoveAt(i);
430 430 continue;
431 431 }
432 432 if (!SpawnedBag[i].IsDestroyed && SpawnedBag[i].deployerUserID == player.userID)
433 433 {
434 434 SpawnedBag[i].Kill();
435 435 SpawnedBag.RemoveAt(i);
436 436 return;
437 437 }
438 438 }
439 439 }
440 440
441 441 private void CheckSpawn(BasePlayer player)
442 442 {
443 443 if (player == null || SpawnedCH47 == null) { return; }
444 444 if (permission.UserHasPermission(player.UserIDString, "CH47SpawnPoint.Force") || player.IsNpc)
445 445 {
446 446 NextFrame(() =>
447 447 {
448 448 if (player != null) //May of died in 1 frame so check again
449 449 {
450 450 player.PauseFlyHackDetection(120);
451 451 player.Teleport(SpawnedCH47.transform.position + SpawnedCH47.transform.up * 1 + SpawnedCH47.transform.forward * -6.25f);
452 452 player.SetParent(SpawnedCH47, true, true);
453 453 player.EndSleeping();
454 454 }
455 455 });
456 456 CleanBags(player);
457 457 return;
458 458 }
459 459 if (Vector3.Distance(player.transform.position, SpawnedCH47.transform.position) < 10)
460 460 {
461 461 NextFrame(() =>
462 462 {
463 463 if (player != null) //May of died in 1 frame so check again
464 464 {
465 465 player.PauseFlyHackDetection(120);
466 466 player.Teleport(SpawnedCH47.transform.position + SpawnedCH47.transform.up * 1 + SpawnedCH47.transform.forward * -6.25f);
467 467 player.SetParent(SpawnedCH47, true, true);
468 468 player.EndSleeping();
469 469 }
470 470 });
471 471 }
472 472 CleanBags(player);
473 473 }
474 474
475 475 void WakeCheck(BasePlayer player)
476 476 {
477 477 if (player == null || SpawnedCH47 == null) { return; }
478 478 if (Vector3.Distance(player.transform.position, SpawnedCH47.transform.position) < 10) //Player waking on ch47
479 479 {
480 480 PutOnParachute(player);
481 481 if (permission.UserHasPermission(player.UserIDString, "CH47SpawnPoint.Mount"))
482 482 {
483 483 if (config.UseNativeSeatsFirst)
484 484 {
485 485 for (int i = SpawnedCH47.mountPoints.Count - 1; i >= 0; i--)
486 486 {
487 487 if (SpawnedCH47.mountPoints[i]?.mountable?.GetMounted() == null)
488 488 {
489 489 SpawnedCH47.mountPoints[i]?.mountable.MountPlayer(player); //Found a mount
490 490 return;
491 491 }
492 492 }
493 493 }
494 494 SpawnMount(player); //Make one
495 495 }
496 496 }
497 497 }
498 498
499 499 private void PutOnParachute(BasePlayer player)
500 500 {
501 501 if (player == null) { return; }
502 502 if (!ParachutingPlayers.ContainsKey(player.userID)) { ParachutingPlayers.Add(player.userID, null); }
503 503 timer.Once(1f, () =>
504 504 {
505 505 if (player == null) { return; } //Check player still exsists after 1 sec
506 506 //Add AutoDeploy
507 507 if (config.AutoDeploy) { AutoChuteChcker acc = player.gameObject.AddComponent<AutoChuteChcker>(); acc._player = player; }
508 508 //Check if player has a parachute and move it to right slot if they do.
509 509 for (int i = player.inventory.containerMain.itemList.Count - 1; i >= 0; i--)
510 510 {
511 511 if (player.inventory.containerMain.itemList[i].info.itemid == 602628465)
512 512 {
513 513 player.inventory.containerWear.Insert(player.inventory.containerMain.itemList[i]);
514 514 player.inventory.containerMain.itemList[i].position = 7;
515 515 player.inventory.containerMain.itemList[i].MarkDirty();
516 516 return; //Has parachute end method
517 517 }
518 518 }
519 519 //Give player a parachute if set in config
520 520 if (config.GiveParachute)
521 521 {
522 522 timer.Once(0.5f, () =>
523 523 {
524 524 if (player != null)
525 525 {
526 526 bool haschute = false;
527 527 List<Item> list = Pool.Get<List<Item>>();
528 528 player.inventory.GetAllItems(list);
529 529 foreach (var i in list) //Check if player has a parachute so doesnt double up
530 530 {
531 531 if (i.info.itemid == 602628465)
532 532 {
533 533 if (config.ForceSingleUseChute)
534 534 {
535 535 i.maxCondition = 0.2f;
536 536 i.conditionNormalized = 0.2f;
537 537 i.MarkDirty();
538 538 }
539 539 haschute = true;
540 540 break;
541 541 }
542 542 }
543 543 Pool.FreeUnmanaged(ref list);
544 544 if (!haschute) //Give new parachute
545 545 {
546 546 Item item = ItemManager.CreateByName("parachute", 1, 0); //Native parachute
547 547 if (item != null && player != null)
548 548 {
549 549 if (config.SingleUseChute)
550 550 {
551 551 item.maxCondition = 0.2f;
552 552 item.conditionNormalized = 0.2f;
553 553 }
554 554 player.inventory.containerWear.Insert(item); //Get around ground check for wearing
555 555 item.position = 7;
556 556 item.MarkDirty();
557 557 }
558 558 }
559 559 }
560 560 });
561 561 }
562 562 });
563 563 }
564 564
565 565 private void SpawnMount(BasePlayer target)
566 566 {
567 567 BaseMountable m = GameManager.server.CreateEntity(StringPool.Get(624857933), target.transform.position) as BaseMountable;
568 568 if (m == null)
569 569 {
570 570 PrintError("Failed to create extra mount entity.");
571 571 return;
572 572 }
573 573 UnityEngine.Object.DestroyImmediate(m.GetComponent<DestroyOnGroundMissing>());
574 574 UnityEngine.Object.DestroyImmediate(m.GetComponent<GroundWatch>());
575 575 foreach (var mesh in m.GetComponentsInChildren<MeshCollider>()) { UnityEngine.Object.DestroyImmediate(mesh); }
576 576 m.OwnerID = target.userID;
577 577 m.enableSaving = false;
578 578 m.Spawn();
579 579 m.SetParent(SpawnedCH47, true, true);
580 580 m.transform.localEulerAngles = new Vector3(0, 180, 0);
581 581 m.MountPlayer(target);
582 582 m.SendNetworkUpdateImmediate();
583 583 ExtraSeats.Add(m);
584 584 }
585 585
586 586 private void CreateSpawnPointHeli()
587 587 {
588 588 Puts("Spawning CH47SpawnPoint Heli");
589 589 SpawnedCH47 = GameManager.server.CreateEntity(CH45Prefab, Vector3.zero + Vector3.up * config.HoverHeight, default(Quaternion)) as CH47HelicopterAIController;
590 590 if (SpawnedCH47 == null)
591 591 {
592 592 PrintError($"Failed to create CH47 from prefab '{CH45Prefab}' — aborting spawn.");
593 593 return;
594 594 }
595 595 SpawnedCH47.Spawn();
596 596 SpawnedCH47.CancelInvoke(SpawnedCH47.CheckSpawnScientists);
597 597 SpawnedCH47.CancelInvoke(SpawnedCH47.SpawnScientists);
598 - SpawnedCH47.SetFlag(BaseEntity.Flags.Reserved8, true);
598 + SpawnedCH47.SetFlagLocal(BaseEntity.Flags.Reserved8, true);
599 599 SpawnedCH47.numCrates = 0;
600 600 Brain = SpawnedCH47.GetComponent<CH47AIBrain>();
601 601 foreach (var mesh in SpawnedCH47.GetComponentsInChildren<MeshCollider>()) { UnityEngine.Object.DestroyImmediate(mesh); }
602 602 foreach (var mesh in SpawnedCH47.GetComponentsInChildren<BoxCollider>()) { UnityEngine.Object.DestroyImmediate(mesh); }
603 603 SpawnedCH47.SetMinHoverHeight(config.HoverHeight);
604 604 if (config.RemoveMapMarker) { if (SpawnedCH47.mapMarkerInstance) { SpawnedCH47.mapMarkerInstance.Kill(BaseNetworkable.DestroyMode.None); } }
605 605 timer.Once(3, () =>
606 606 {
607 607 if (config.SafeZoneHeli) { SpawnedCH47.gameObject.AddComponent<SafeZoneTrigger>(); }
608 608 foreach (var item in SpawnedCH47.allMountPoints)
609 609 {
610 610 BasePlayer npc = item.mountable.GetMounted();
611 611 if (npc != null && npc.IsNpc && !npc.IsDestroyed) { npc.Kill(); }
612 612 }
613 613 if (Brain != null)
614 614 {
615 615 Brain.states = new Dictionary<AIState, BaseAIBrain.BasicAIState>();
616 616 Brain.AddState(new CH47AIBrain.PatrolState());
617 617 Brain.AddState(new CH47AIBrain.OrbitState());
618 618 Brain.mainInterestPoint = RandomPoint(SpawnedCH47.transform.position);
619 619 }
620 620 timer.Once(5, () => { foreach (BasePlayer bp in BasePlayer.allPlayerList) { if (!bp.IsNpc && bp.IsConnected && bp.IsDead()) { MakeBag(bp); } } });
621 621 });
622 622 bool triedResetToCenter = false;
623 623 SpawnedTimer = timer.Every(config.HeliTick, () =>
624 624 {
625 625 try
626 626 {
627 - foreach(var state in Brain.states)
627 + foreach (var state in Brain.states)
628 628 {
629 629 if (state.Value != null)
630 630 {
631 631 state.Value.TimeInState = 0;//publicized by oxide compiler
632 632 }
633 633 }
634 634 if (SpawnedCH47 == null)
635 635 {
636 636 Puts("SpawnedCH47 is null");
637 637 }
638 638 else if (SpawnedCH47.transform == null)
639 639 {
640 640 Puts("SpawnedCH47.transform is null");
641 641 }
642 642
643 643 if (Brain == null)
644 644 {
645 645 Puts("Brain is null");
646 646 }
647 647 else if (Brain.CurrentState == null)
648 648 {
649 649 Puts("Brain.CurrentState is null");
650 650 }
651 651 if (SpawnedCH47 == null || Brain == null || Brain.CurrentState == null || SpawnedCH47.transform == null)
652 652 {
653 653 UnloadPlugin();
654 654 CreateSpawnPointHeli();
655 655 return;
656 656 }
657 657
658 658 Brain.CurrentState.Reset();
659 659 Brain.mainInterestPoint = RandomPoint(SpawnedCH47.transform.position);
660 660
661 661 // Check if the heli has moved
662 662 Vector3 currentPos = SpawnedCH47.transform.position;
663 663
664 664 if (currentPos == oldPos)
665 665 {
666 666 if (!triedResetToCenter)
667 667 {
668 668 // Try one last time — go to map center
669 669 Puts("Heli seems stuck, sending it to map center as last attempt...");
670 670 triedResetToCenter = true;
671 671 Brain.mainInterestPoint = Vector3.zero + Vector3.up * config.HoverHeight;
672 672 }
673 673 else
674 674 {
675 675 // Still stuck — kill it
676 676 Puts("Heli stuck again after reset attempt — killing it.");
677 677 SpawnedCH47.Kill();
678 678 SpawnedCH47 = null;
679 679 UnloadPlugin();
680 680 CreateSpawnPointHeli();
681 681 }
682 682 }
683 683 else
684 684 {
685 685 // Update last known position
686 686 oldPos = currentPos;
687 687 }
688 688 }
689 689 catch (Exception ex)
690 690 {
691 691 Puts($"Error in SpawnedTimer: {ex.Message}");
692 692 }
693 693 });
694 694 }
695 695
696 696 private Vector3 RandomPoint(Vector3 target, int tries = 0)
697 697 {
698 698 float scanSize = (World.Size / 2f) * config.ScanSize;
699 699 float stepSize = scanSize / 4f;
700 700 const int maxTries = 10; // more attempts
701 701 const int samplesPerTry = 12; // more samples per try
702 702
703 703 List<Vector3> points = Pool.Get<List<Vector3>>();
704 704 points.Clear();
705 705
706 706 try
707 707 {
708 708 for (; tries < maxTries; tries++)
709 709 {
710 710 float radius = scanSize - (stepSize * tries);
711 711
712 712 for (int i = 0; i < samplesPerTry; i++)
713 713 {
714 714 float angle = UnityEngine.Random.Range(0f, 360f);
715 715 Vector3 offset = new Vector3(
716 716 Mathf.Sin(angle * Mathf.Deg2Rad),
717 717 0f,
718 718 Mathf.Cos(angle * Mathf.Deg2Rad)
719 719 ) * radius;
720 720
721 721 Vector3 candidate = target + offset;
722 722
723 723 // Optional: constrain to allowed range
724 724 if (config.MaxDistance > 0 && Vector3.Distance(candidate, Vector3.zero) >= config.MaxDistance)
725 725 continue;
726 726
727 727 float height = TerrainMeta.HeightMap.GetHeight(candidate);
728 728 if (height >= 1f && height <= config.HoverHeight)
729 729 {
730 730 points.Add(candidate);
731 731 if (points.Count >= 8)
732 732 return points.GetRandom(); // early return on success
733 733 }
734 734 }
735 735 }
736 736
737 737 // fallback: if we found at least one valid point, pick one randomly
738 738 if (points.Count > 0)
739 739 return points.GetRandom();
740 740
741 741 // last resort fallback
742 742 return TerrainMeta.Path.Monuments.GetRandom()?.transform.position ?? target;
743 743 }
744 744 finally
745 745 {
746 746 Pool.FreeUnmanaged(ref points);
747 747 }
748 748 }
749 749 #endregion
750 750
751 751 #region Harmony
752 752 [AutoPatch]
753 753 [HarmonyPatch(typeof(CH47HelicopterAIController), "CalculateOverrideAltitude")]
754 754 public static class CalculateOverrideAltitude
755 755 {
756 756 [HarmonyPostfix]
757 757 private static void Postfix(CH47HelicopterAIController __instance)
758 758 {
759 759 try
760 760 {
761 761 if (plugin.SpawnedCH47 != __instance) { return; }
762 762 if (__instance.transform.position.y > plugin.config.MaxHoverHeight) { __instance.currentDesiredAltitude = plugin.config.MaxHoverHeight; __instance.altOverride = plugin.config.MaxHoverHeight; }
763 763 }
764 764 catch { }
765 765 }
766 766 }
767 767
768 768 [AutoPatch]
769 769 [HarmonyPatch(typeof(Parachute), "IsValidSAMTarget", typeof(bool))]
770 770 internal class Parachute_IsValidSAMTarget
771 771 {
772 772 [HarmonyPostfix]
773 773 static void Postfix(Parachute __instance, ref bool __result)
774 774 {
775 775 try
776 776 {
777 777 if (!plugin.config.IgnoreSpawnChute)
778 778 {
779 779 try { __result = plugin.ParachutingPlayers.ContainsValue(__instance); } catch { }
780 780 }
781 781 }
782 782 catch { }
783 783 }
784 784 }
785 785
786 786 [AutoPatch]
787 787 [HarmonyPatch(typeof(Parachute), "PlayerMounted", typeof(BasePlayer), typeof(BaseMountable))]
788 788 internal class Parachute_PlayerMounted
789 789 {
790 790 [HarmonyPostfix]
791 791 static void Postfix(BasePlayer player, BaseMountable seat, Parachute __instance)
792 792 {
793 793 try
794 794 {
795 795 if (plugin.ParachutingPlayers.ContainsKey(player.userID)) //Drop heli parachute
796 796 {
797 797 plugin.ParachutingPlayers[player.userID] = __instance;
798 798 __instance.skinID = plugin.config.SkinID;
799 799 __instance.HurtAmount = plugin.config.HurtAmount;
800 800 __instance.UprightLerpForce = plugin.config.UprightLerpForce;
801 801 __instance.ConstantForwardForce = plugin.config.ConstantForwardForce;
802 802 __instance.TurnForce = plugin.config.TurnForce;
803 803 __instance.ForwardTiltAcceleration = plugin.config.ForwardTiltAcceleration;
804 804 __instance.BackInputForceMultiplier = plugin.config.BackInputForceMultiplier;
805 805 __instance.TargetDrag = plugin.config.TargetDrag;
806 806 __instance.TargetAngularDrag = plugin.config.TargetAngularDrag;
807 807 __instance.SendNetworkUpdateImmediate();
808 808 }
809 809 }
810 810 catch { }
811 811 }
812 812 }
813 813
814 814 [AutoPatch]
815 815 [HarmonyPatch(typeof(Parachute), "PlayerDismounted", typeof(BasePlayer), typeof(BaseMountable))]
816 816 internal class Parachute_PlayerDismounted
817 817 {
818 818 [HarmonyPrefix]
819 819 static bool Prefix(BasePlayer player, BaseMountable seat, Parachute __instance, ref bool ___collisionDeath, TimeSince ___mountTime)
820 820 {
821 821 try
822 822 {
823 823 if (plugin.ParachutingPlayers.ContainsKey(player.userID))
824 824 {
825 825 plugin.ParachutingPlayers.Remove(player.userID);
826 826 if (plugin.config.DisableCollisionHurt) { ___collisionDeath = false; }
827 827 if (plugin.config.ForceRemove)
828 828 {
829 829 seat.DismountAllPlayers();
830 830 if (__instance.collisionDeath && !plugin.config.DisableCollisionHurt)
831 831 {
832 832 if (___mountTime < __instance.HurtDeployTime)
833 833 {
834 834 float num = 1f - Mathf.Clamp01(___mountTime / __instance.HurtDeployTime);
835 835 player.Hurt(__instance.HurtAmount * num, Rust.DamageType.Fall, null, true);
836 836 }
837 837 else
838 838 {
839 839 float magnitude = __instance.collisionImpulse.magnitude;
840 840 if (magnitude > 50f)
841 841 {
842 842 float num2 = Mathx.RemapValClamped(magnitude, 50f, 400f, 5f, 50f);
843 843 player.Hurt(num2, Rust.DamageType.Fall, null, true);
844 844 }
845 845 }
846 846 }
847 847 if (__instance.collisionDeath && Parachute.LandingAnimations)
848 848 {
849 849 Effect.server.Run(__instance.ParachuteLandScreenBounce.resourcePath, player, 0U, Vector3.zero, Vector3.zero, null, false, null);
850 850 if (__instance.collisionLocalPos.y < 0.15f)
851 851 {
852 852 player.Server_StartGesture(GestureCollection.HeavyLandingId);
853 853 player.PlayHeavyLandingAnimation = false;
854 854 }
855 855 }
856 856 __instance.Kill();
857 857 return false;
858 858 }
859 859
860 860 }
861 861 }
862 862 catch { }
863 863 return true;
864 864 }
865 865 }
866 866 #endregion
867 867
868 868 #region Classes
869 869 public class AutoChuteChcker : MonoBehaviour
870 870 {
871 871 public BasePlayer _player;
872 872 private byte delay = 0;
873 873 public void RemoveMe() { Destroy(this); }
874 874 public void FixedUpdate()
875 875 {
876 876 if (plugin == null || _player == null) { try { RemoveMe(); } catch { } return; } //No player or no plugin remove
877 877 if (delay <= plugin.config.AutoDeployTick) { delay++; return; } //Delay switch
878 878 if (!_player.IsConnected || _player.IsDead()) { RemoveMe(); return; } //Player disconnected or dead remove.
879 879 if (_player.IsSleeping() || _player.isMounted) { return; } //On Parachute/Ch47 or still sleeping Do nothing
880 880 delay = 0;
881 881 //Check trigger height
882 882 if (_player.transform.position.y <= 30 || _player.transform.position.y <= TerrainMeta.HeightMap.GetHeight(_player.transform.position) + plugin.config.AutoDeployHeight)
883 883 {
884 884 _player.RequestParachuteDeploy();
885 885 RemoveMe();
886 886 }
887 887 }
888 888 }
889 889
890 890 public class SafeZoneTrigger : MonoBehaviour
891 891 {
892 892 public void RemoveMe() => Destroy(this);
893 893 public void FixedUpdate()
894 894 {
895 - if (plugin == null || plugin?.Brain == null) { try { RemoveMe(); } catch { } return; }
896 - foreach (var p in plugin?.SpawnedCH47?.mountPoints) { if (p?.mountable?.GetMounted() != null) { p?.mountable?.GetMounted()?.SetPlayerFlag(BasePlayer.PlayerFlags.SafeZone, true); } }
897 - foreach (var p in plugin?.ExtraSeats) { if (p?.GetMounted() != null) { p?.GetMounted()?.SetPlayerFlag(BasePlayer.PlayerFlags.SafeZone, true); } }
895 + if (plugin == null || plugin?.Brain == null) { try { RemoveMe(); } catch { } return; }
896 + foreach (var p in plugin?.SpawnedCH47?.mountPoints) { if (p?.mountable?.GetMounted() != null) { p?.mountable?.GetMounted()?.SetPlayerFlag(BasePlayer.PlayerFlags.SafeZone, true); } }
897 + foreach (var p in plugin?.ExtraSeats) { if (p?.GetMounted() != null) { p?.GetMounted()?.SetPlayerFlag(BasePlayer.PlayerFlags.SafeZone, true); } }
898 898 }
899 899 }
900 900 #endregion
901 901 }
902 902 }