← 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
}