BHD2 and infantry update (#48)

* BHD2 and infantry update

- default to easy comms
- modify ctld for better infantry models
- cleaner logging
- warn user if no resources for fat cow FARPs
- condition for fat cow added; not available if enemies too close

* Update README.md
This commit is contained in:
spencershepard 2023-02-11 20:54:37 -08:00 committed by GitHub
parent be89639e6d
commit bca47d63d6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 153 additions and 53 deletions

View File

@ -593,6 +593,7 @@ class Window(QMainWindow, Ui_MainWindow):
"blue_cap": self.scenario.getConfigValue("blue_cap", default=True),
"rotorops_server": self.scenario.getConfigValue("rotorops_server", default=False),
"perks": self.perks_checkBox.isChecked(),
"easy_comms": self.scenario.getConfigValue("easy_comms", default=True)
}
logger.info("Generating mission with options:")

View File

@ -401,6 +401,12 @@ class RotorOpsMission:
self.m.random_daytime(options["time"].lower())
print("Time set to " + options["time"])
# set the mission options
if options["easy_comms"]:
# to simplify rearm/refuel at FARPs
self.m.options.difficulty.easyCommunication = True
# Save the mission file
window.statusBar().showMessage("Saving mission...", 10000)
if window.user_output_dir:
@ -824,7 +830,7 @@ class RotorOpsMission:
self.m.triggers.add_triggerzone(f_cap_spawn_point, 30000, hidden=True, name="BLUE_CAP_SPAWN")
# Fat Cow
if True:
if options["perks"]:
helo_type = dcs.helicopters.CH_47D
name = "FAT COW"
@ -852,6 +858,24 @@ class RotorOpsMission:
afg.set_skill(dcs.unit.Skill.Excellent)
afg.late_activation = True
else:
afg = self.m.flight_group_inflight(
combinedJointTaskForcesBlue,
name,
helo_type,
position=primary_f_airport.position,
altitude=500,
speed=50,
group_size=1
)
if afg:
afg.set_skill(dcs.unit.Skill.Excellent)
afg.late_activation = True
else:
raise Exception("Unable to insert Fat Cow CH-47")
if options["f_awacs"]:
awacs_name = "AWACS"

View File

@ -1,7 +1,7 @@
# ROTOROPS VERSION
maj_version = 1
minor_version = 4
patch_version = 3
patch_version = 4
version_url = 'https://dcs-helicopters.com/app-updates/versioncheck.yaml'

View File

@ -24,15 +24,24 @@ At the core of the RotorOps script are AI enhancements that provide a dynamic gr
- Single-player and multiplayer slot creation.
## Demo Missions
RotorOps: Aleppo Under Siege https://www.digitalcombatsimulator.com/en/files/3320079/
Newest to oldest:
Black Hawk Down Pt 1 (UH-1H UH-60L) https://www.digitalcombatsimulator.com/en/files/3328428/
NightHawks (AH-64D) https://www.digitalcombatsimulator.com/en/files/3322036/
RotorOps: Aleppo Under Siege https://www.digitalcombatsimulator.com/en/files/3320079/
Rota Landing (Mr. Nobody) https://www.digitalcombatsimulator.com/en/files/3320186/
# RotorOps: Conflict
Conflict is a game type in which attacking forces must clear Conflict Zones of defending ground forces. Once a zone is cleared, the next zone is activated and attacking ground vehicles will move to the next Conflict Zone automatically.
![alt text](https://raw.githubusercontent.com/spencershepard/RotorOps/develop/documentation/images/rotorops%20conflict%20zones.png?raw=true)
![alt text](https://raw.githubusercontent.com/spencershepard/RotorOps/main/documentation/images/rotorops%20conflict%20zones.png?raw=true)
## Dynamic Roles
A RotorOps Conflict mission has opportunities for a variety of roles and tasks. There's no need to artificially select these roles, as the mission is fully dynamic.

View File

@ -26,7 +26,7 @@ ctld = {} -- DONT REMOVE!
ctld.Id = "CTLD - "
--- Version.
ctld.Version = "20211113.01"
ctld.Version = "20211113.01 GRIMM01"
-- debug level, specific to this module
ctld.Debug = true
@ -1976,9 +1976,9 @@ function ctld.generateTroopTypes(_side, _countOrTemplate, _country)
if _countOrTemplate.inf then
ctld.logTrace(string.format("_countOrTemplate.inf=%s", ctld.p(_countOrTemplate.inf)))
if _side == 2 then
_troops = ctld.insertIntoTroopsArray("Soldier M4",_countOrTemplate.inf,_troops)
_troops = ctld.insertIntoTroopsArray("Soldier M4 GRG",_countOrTemplate.inf,_troops)
else
_troops = ctld.insertIntoTroopsArray("Soldier AK",_countOrTemplate.inf,_troops)
_troops = ctld.insertIntoTroopsArray("Infantry AK",_countOrTemplate.inf,_troops)
end
_weight = _weight + getSoldiersWeight(_countOrTemplate.inf, ctld.RIFLE_WEIGHT)
ctld.logTrace(string.format("_weight=%s", ctld.p(_weight)))
@ -2012,9 +2012,9 @@ function ctld.generateTroopTypes(_side, _countOrTemplate, _country)
if _countOrTemplate.jtac then
ctld.logTrace(string.format("_countOrTemplate.jtac=%s", ctld.p(_countOrTemplate.jtac)))
if _side == 2 then
_troops = ctld.insertIntoTroopsArray("Soldier M4",_countOrTemplate.jtac,_troops, "JTAC")
_troops = ctld.insertIntoTroopsArray("Soldier M4 GRG",_countOrTemplate.jtac,_troops, "JTAC")
else
_troops = ctld.insertIntoTroopsArray("Soldier AK",_countOrTemplate.jtac,_troops, "JTAC")
_troops = ctld.insertIntoTroopsArray("Infantry AK",_countOrTemplate.jtac,_troops, "JTAC")
end
_hasJTAC = true
_weight = _weight + getSoldiersWeight(_countOrTemplate.jtac, ctld.JTAC_WEIGHT + ctld.RIFLE_WEIGHT)
@ -2024,7 +2024,7 @@ function ctld.generateTroopTypes(_side, _countOrTemplate, _country)
else
for _i = 1, _countOrTemplate do
local _unitType = "Soldier AK"
local _unitType = "Infantry AK"
if _side == 2 then
if _i <=2 then
@ -2040,7 +2040,7 @@ function ctld.generateTroopTypes(_side, _countOrTemplate, _country)
_weight = _weight + getSoldiersWeight(1, ctld.MANPAD_WEIGHT)
ctld.logTrace(string.format("_unitType=%s, _weight=%s", ctld.p(_unitType), ctld.p(_weight)))
else
_unitType = "Soldier M4"
_unitType = "Soldier M4 GRG"
_weight = _weight + getSoldiersWeight(1, ctld.RIFLE_WEIGHT)
ctld.logTrace(string.format("_unitType=%s, _weight=%s", ctld.p(_unitType), ctld.p(_weight)))
end

View File

@ -1,6 +1,6 @@
RotorOps = {}
RotorOps.version = "1.3.4"
local debug = true
local debug = false
@ -65,7 +65,7 @@ RotorOps.game_state = 0
RotorOps.zones = {}
RotorOps.active_zone = "" --name of the active zone
RotorOps.active_zone_index = 0
RotorOps.game_state_flag = 1 --user flag to store the game state
RotorOps.game_state_flag = 100 --user flag to store the game state
RotorOps.staging_zones = {}
RotorOps.ai_defending_infantry_groups = {}
RotorOps.ai_attacking_infantry_groups = {}
@ -1200,6 +1200,7 @@ function RotorOps.assessUnitsInZone(var)
--RotorOps.inf_spawns_avail = RotorOps.inf_spawns_per_zone * RotorOps.inf_spawn_multiplier[RotorOps.active_zone_index]
if total_spawn_zones > 0 then
RotorOps.inf_spawns_avail = (RotorOps.inf_spawns_total / total_spawn_zones) * #inf_spawn_zones
RotorOps.inf_spawns_avail = math.ceil(RotorOps.inf_spawns_avail)
end
env.info("ROTOR OPS: zone activated: "..RotorOps.active_zone..", inf spawns avail:"..RotorOps.inf_spawns_avail..", spawn zones:"..#inf_spawn_zones)
@ -1285,7 +1286,8 @@ function RotorOps.assessUnitsInZone(var)
for index, vehicle in pairs(units_table) do
local should_deploy = false
if vehicle:hasAttribute("Infantry carriers") and RotorOps.isUnitInZone(vehicle, RotorOps.active_zone) then --if a vehicle is an APC and in zone
if vehicle:hasAttribute("Infantry carriers") or vehicle:hasAttribute("Trucks") then --if a vehicle is an APC
if RotorOps.isUnitInZone(vehicle, RotorOps.active_zone) then --if a vehicle is an APC and in zone
local apc_name = vehicle:getName()
if tableHasKey(apcs, apc_name) == true then --if we have this apc in our table already
@ -1303,6 +1305,7 @@ function RotorOps.assessUnitsInZone(var)
should_deploy = true
apcs[apc_name] = {['deployed_zones'] = {RotorOps.active_zone,}}
end
end
end
@ -1314,7 +1317,7 @@ function RotorOps.assessUnitsInZone(var)
end
end
local id = timer.scheduleFunction(timedDeploy, nil, timer.getTime() + math.random(90, 180))
local id = timer.scheduleFunction(timedDeploy, nil, timer.getTime() + math.random(90, 300))
end
end
@ -1332,7 +1335,9 @@ function RotorOps.assessUnitsInZone(var)
local zone = inf_spawn_zones[rand_index]
ctld.spawnGroupAtTrigger("red", RotorOps.inf_spawn_red, zone, 1000)
RotorOps.gameMsg(RotorOps.gameMsgs.infantry_spawned, math.random(1, #RotorOps.gameMsgs.infantry_spawned))
if RotorOps.inf_spawn_messages then
RotorOps.gameMsg(RotorOps.gameMsgs.infantry_spawned, math.random(1, #RotorOps.gameMsgs.infantry_spawned))
end
RotorOps.inf_spawns_avail = RotorOps.inf_spawns_avail - 1
env.info("ROTOR OPS: Attempting to spawn infantry. "..RotorOps.inf_spawns_avail.." spawns remaining in "..zone)
@ -2226,3 +2231,4 @@ function RotorOps.predSpawnRedCap()
return true
end

View File

@ -9,13 +9,13 @@
-- Issues:
-- - You will not get points for your troops' kills if you leave your group (ie switch aircraft)
-- - Currently requires a modified version of MIST (see rotorops repo /scripts)
--Todo:
-- - more testing needed in RotorOpsPerks.monitorFarps() to check for destroyed farp objects.
RotorOpsPerks = {}
RotorOpsPerks.version = "1.5.1"
RotorOpsPerks.version = "1.5.2"
env.warning('ROTOROPS PERKS STARTED: '..RotorOpsPerks.version)
trigger.action.outText('ROTOROPS PERKS STARTED: '..RotorOpsPerks.version, 10)
RotorOpsPerks.perks = {}
@ -52,11 +52,17 @@ RotorOpsPerks.player_fatcow_types = {
}
---- END OPTIONS ----
local function log(msg)
env.info("ROTOROPS PERKS: " .. msg)
end
local function debugMsg(msg)
if RotorOpsPerks.debug then
env.info("ROTOROPS PERKS:")
env.info(msg)
log("ROTOROPS PERKS:")
if msg then
log(msg)
end
end
end
@ -91,7 +97,28 @@ end
RotorOpsPerks.perks.fatcow["action_condition"] = function(args)
if #RotorOpsPerks.fat_cow_farps < 1 then
return {msg="No FARP resources available!", valid=false}
end
end
--rearming/refueling doesn't work if enemies are nearby (within 1.1nm)
--this is a DCS feature/limitation so we won't deploy the farp to avoid confusing the players
local units_in_proximity = RotorOpsPerks.findUnitsInVolume({
volume_type = world.VolumeType.SPHERE,
point = args.target_point,
radius = 2050
})
log("units_in_proximity: "..#units_in_proximity)
local enemy_coal = 1
if args.player_coalition == 1 then
enemy_coal = 2
end
for _, unit in pairs(units_in_proximity) do
if unit:getCoalition() == enemy_coal then
return {msg="Too close to enemy!", valid=false}
end
end
return {valid=true}
end
@ -267,17 +294,42 @@ end
RotorOpsPerks.perks.player_fatcow["action_condition"] = function(args)
local player_unit = Group.getByName(args.player_group_name):getUnit(1)
local agl_altitude = player_unit:getPosition().p.y - land.getHeight(player_unit:getPosition().p)
if RotorOpsPerks.perks.player_fatcow.active[args.player_group_name] then
return {msg="FARP already deployed at your position!", valid=false}
end
if #RotorOpsPerks.fat_cow_farps < 1 then
return {msg="No FARP resources available!", valid=false}
end
if agl_altitude > 100 then
return {msg="You must be on the ground! "..agl_altitude.." AGL", valid=false}
else
return {msg="Stay on the ground.", valid=true}
end
--rearming/refueling doesn't work if enemies are nearby (within 1.1nm)
--this is a DCS feature/limitation so we won't deploy the farp to avoid confusing the players
local units_in_proximity = RotorOpsPerks.findUnitsInVolume({
volume_type = world.VolumeType.SPHERE,
point = args.target_point,
radius = 2050
})
log("units_in_proximity: "..#units_in_proximity)
local enemy_coal = 1
if args.player_coalition == 1 then
enemy_coal = 2
end
for _, unit in pairs(units_in_proximity) do
if unit:getCoalition() == enemy_coal then
return {msg="Too close to enemy!", valid=false}
end
end
return {msg="Stay on the ground.", valid=true}
end
RotorOpsPerks.perks.player_fatcow["action_function"] = function(args)
@ -302,10 +354,10 @@ RotorOpsPerks.perks.player_fatcow["monitor_player"] = function(args)
local agl_altitude = player_unit:getPosition().p.y - land.getHeight(player_unit:getPosition().p)
if agl_altitude > 100 or not player_unit:isExist() then
despawn_farp = true
env.info("Player is no longer on the ground, despawning FARP!")
log("Player is no longer on the ground, despawning FARP!")
end
if math.abs(player_unit:getPosition().p.x - args.target_point.x) > 50 or math.abs(player_unit:getPosition().p.z - args.target_point.z) > 50 then
env.info("Player has moved from target position, despawning FARP!")
log("Player has moved from target position, despawning FARP!")
despawn_farp = true
end
else
@ -435,8 +487,8 @@ function RotorOpsPerks.checkPoints(player_group_name)
return false
end
env.info("Checking points for "..player_group_name.."...")
env.info(mist.utils.tableShow(players, "players"))
log("Checking points for "..player_group_name.."...")
log(mist.utils.tableShow(players, "players"))
--get combined points from all Players
local total_points = 0
@ -510,7 +562,7 @@ function RotorOpsPerks.updatePlayer(identifier, groupName, name, slot)
perks_used = {},
}
env.warning('ADDED ' .. identifier .. ' TO PLAYERS TABLE')
env.info(mist.utils.tableShow(RotorOpsPerks.players[identifier]))
log(mist.utils.tableShow(RotorOpsPerks.players[identifier]))
missionCommands.removeItemForGroup(groupId, {[1] = 'ROTOROPS PERKS'})
RotorOpsPerks.addRadioMenuForGroup(groupName)
if RotorOpsPerks.player_update_messages then
@ -520,7 +572,7 @@ function RotorOpsPerks.updatePlayer(identifier, groupName, name, slot)
--update an existing player
elseif RotorOpsPerks.players[identifier].groupId ~= groupId then
env.warning('UPDATING ' .. identifier .. ' TO GROUP NAME: ' .. groupName)
env.info(mist.utils.tableShow(RotorOpsPerks.players[identifier]))
log(mist.utils.tableShow(RotorOpsPerks.players[identifier]))
if RotorOpsPerks.player_update_messages then
trigger.action.outText('PERKS: ' .. name .. ' moved to '.. groupName, 10)
end
@ -590,7 +642,7 @@ end
---- FATCOW FARP SUPPORTING FUNCTIONS ----
function RotorOpsPerks.monitorFarps()
env.info(mist.utils.tableShow(RotorOpsPerks.fat_cow_farps))
--log(mist.utils.tableShow(RotorOpsPerks.fat_cow_farps))
local function farpExists(i)
local farp = StaticObject.getByName('FAT COW FARP ' .. i)
@ -627,7 +679,7 @@ function RotorOpsPerks.buildFatCowFarpTable()
local ammo = StaticObject.getByName('FAT COW AMMO ' .. i)
local fuel = StaticObject.getByName('FAT COW FUEL ' .. i)
if farp and tent and ammo and fuel then
env.info("FAT COW FARP " .. i .. " FOUND")
log("FAT COW FARP " .. i .. " FOUND")
RotorOpsPerks.fat_cow_farps[i] = {
index = i,
farp = farp,
@ -655,7 +707,7 @@ function RotorOpsPerks.teleportStatic(source_name, dest_point)
debugMsg('RotorOpsPerks.teleportStatic: ' .. source_name)
local source = StaticObject.getByName(source_name)
if not source then
env.info('RotorOpsPerks.teleportStatic: source not found: ' .. source_name)
log('RotorOpsPerks.teleportStatic: source not found: ' .. source_name)
return
end
local vars = {}
@ -664,14 +716,14 @@ function RotorOpsPerks.teleportStatic(source_name, dest_point)
vars.point = mist.utils.makeVec3(dest_point)
local res = mist.teleportToPoint(vars)
if res then
env.info('RotorOpsPerks.teleportStatic: ' .. source_name .. ' success')
log('RotorOpsPerks.teleportStatic: ' .. source_name .. ' success')
else
env.info('RotorOpsPerks.teleportStatic: ' .. source_name .. ' failed')
log('RotorOpsPerks.teleportStatic: ' .. source_name .. ' failed')
end
end
function RotorOpsPerks.spawnFatCowFarpObjects(pt_x, pt_y, index, delay)
env.info('spawnFatCowFarpObjects called. Looking for static group names ending in ' .. index)
log('spawnFatCowFarpObjects called. Looking for static group names ending in ' .. index)
local dest_point = mist.utils.makeVec3GL({x = pt_x, y = pt_y})
trigger.action.smoke(dest_point, 2)
@ -693,12 +745,12 @@ function RotorOpsPerks.spawnFatCow(dest_point, farp)
local fatcow_name = 'FAT COW'
local source_farp_name = 'FAT COW FARP ' .. index
env.info('spawnFatCow called with ' .. source_farp_name)
log('spawnFatCow called with ' .. source_farp_name)
--set a timer to return the farp static resources to be reused
timer.scheduleFunction(function()
table.insert(RotorOpsPerks.fat_cow_farps, farp) --put it back at the end of the list
env.info('FatCow FARP timer expired, making the farp available to be used again.')
log('FatCow FARP timer expired, making the farp available to be used again.')
end, nil, timer.getTime() + 1800)
dest_point = mist.utils.makeVec2(dest_point)
@ -793,8 +845,8 @@ function RotorOpsPerks.spawnFatCow(dest_point, farp)
end
function RotorOpsPerks.requestPerk(args)
env.info('requestPerk called for ' .. args.perk_name)
--env.info(mist.utils.tableShow(args, 'args'))
log('requestPerk called for ' .. args.perk_name)
--log(mist.utils.tableShow(args, 'args'))
local player_group = Group.getByName(args.player_group_name)
local player_unit = player_group:getUnits()[1]
local player_unit_name = player_unit:getName()
@ -834,7 +886,7 @@ function RotorOpsPerks.requestPerk(args)
mark_name = mark_name:gsub("\n", "")
if mark_name == args.perk_name then
perk_name_matches = true
env.info("mark name matches perk name")
log("mark name matches perk name")
end
if perk_name_matches then
@ -866,9 +918,9 @@ function RotorOpsPerks.requestPerk(args)
end
end
-- env.info(mist.utils.tableShow(mist.DBs.markList, 'markList'))
-- env.info('player group' .. mist.utils.tableShow(player_group, 'player_group'))
-- env.info('player' .. mist.utils.tableShow(player_unit, 'player_unit'))
-- log(mist.utils.tableShow(mist.DBs.markList, 'markList'))
-- log('player group' .. mist.utils.tableShow(player_group, 'player_group'))
-- log('player' .. mist.utils.tableShow(player_unit, 'player_unit'))
if temp_mark then
target_point = temp_mark.pos
end
@ -923,9 +975,10 @@ function RotorOpsPerks.requestPerk(args)
args.player_group = player_group
args.player_unit = player_unit
args.player_unit_name = player_unit_name
args.player_coalition = player_group:getCoalition()
--show all variables available to perk actions and conditions
--env.info('args: ' .. mist.utils.tableShow(args, 'args'))
--log('args: ' .. mist.utils.tableShow(args, 'args'))
--check the perk's unique prerequisite conditions
@ -947,9 +1000,9 @@ function RotorOpsPerks.requestPerk(args)
--check points
if RotorOpsPerks.spendPoints(args.player_group_name, perk.cost, false) then
env.info(args.player_group_name.. ' has sufficient (' .. perk.cost .. ') points for ' .. args.perk_name)
log(args.player_group_name.. ' has sufficient (' .. perk.cost .. ') points for ' .. args.perk_name)
else
env.info(args.player_group_name.. ' tried to spend ' .. perk.cost .. ' points for ' .. args.perk_name .. ' but did not have enough points')
log(args.player_group_name.. ' tried to spend ' .. perk.cost .. ' points for ' .. args.perk_name .. ' but did not have enough points')
if #players == 1 then
trigger.action.outTextForGroup(player_group:getID(), 'NEGATIVE. You have ' .. RotorOpsPerks.getPlayerGroupSum(args.player_group_name, "points") .. ' points. (cost '.. perk.cost .. ')', 10)
else
@ -996,7 +1049,7 @@ function RotorOpsPerks.requestPerk(args)
trigger.action.outTextForGroup(_player.groupId, 'AFFIRM. ' .. RotorOpsPerks.perks[args.perk_name].display_name .. position_string, 10)
else
-- send messages to all other players
env.info(player_unit:getPlayerName() .. ' requested ' .. RotorOpsPerks.perks[args.perk_name].display_name .. position_string)
log(player_unit:getPlayerName() .. ' requested ' .. RotorOpsPerks.perks[args.perk_name].display_name .. position_string)
trigger.action.outTextForGroup(_player.groupId, player_unit:getPlayerName() .. ' requested ' .. RotorOpsPerks.perks[args.perk_name].display_name .. position_string, 10)
end
end
@ -1157,11 +1210,11 @@ function RotorOpsPerks.registerCtldCallbacks()
local unit = _args.unit
local picked_troops = _args.onboard
local dropped_troops = _args.unloaded
--env.info("ctld callback: ".. mist.utils.tableShow(_args))
--log("ctld callback: ".. mist.utils.tableShow(_args))
if dropped_troops then
--env.info('dropped troops: ' .. mist.utils.tableShow(dropped_troops))
--env.info('dropped troops group name: ' .. dropped_troops:getName())
--log('dropped troops: ' .. mist.utils.tableShow(dropped_troops))
--log('dropped troops group name: ' .. dropped_troops:getName())
RotorOpsPerks.troops[dropped_troops:getName()] = {dropped_troops=dropped_troops:getName(), player_group=unit:getGroup():getName(), player_name=unit:getPlayerName(), player_unit=unit:getName(), side=unit:getGroup():getCoalition() , qty=#dropped_troops:getUnits()}
end
@ -1253,16 +1306,23 @@ function RotorOpsPerks.monitorPlayers()
end
if mist.grimm_version then
env.info("GRIMM's version of MIST is loaded")
log("GRIMM's version of MIST is loaded")
else
env.warning("ROTOROPS PERKS REQUIRES A MODIFIED VERSION OF MIST TO WORK PROPERLY. PLEASE SEE THE SCRIPTS FOLDER IN THE ROTOROPS GITHUB REPO")
env.warning("ERROR: ROTOROPS PERKS REQUIRES A MODIFIED VERSION OF MIST TO WORK PROPERLY. PLEASE SEE THE SCRIPTS FOLDER IN THE ROTOROPS GITHUB REPO")
trigger.action.outText("ERROR: ROTOROPS PERKS REQUIRES A MODIFIED VERSION OF MIST TO WORK PROPERLY.", 30)
end
RotorOpsPerks.buildFatCowFarpTable()
env.info("Found " .. #RotorOpsPerks.fat_cow_farps .. " Fat Cow FARPs")
log("Found " .. #RotorOpsPerks.fat_cow_farps .. " Fat Cow FARPs")
if #RotorOpsPerks.fat_cow_farps > 0 then
RotorOpsPerks.monitorFarps()
else
env.warning("NO FAT COW FARPS FOUND. PLEASE SEE THE ROTOROPS WIKI FOR INSTRUCTIONS ON HOW TO SET UP FAT COW FARPS")
trigger.action.outText("WARNING: NO FAT COW FARPS FOUND.", 30)
end
if not Group.getByName('FAT COW') then
env.warning("NO AI FAT COW HELICOPTER FOUND. PLEASE SEE THE ROTOROPS WIKI FOR INSTRUCTIONS ON HOW TO SET UP FAT COW FARPS")
trigger.action.outText("WARNING: NO AI FAT COW HELICOPTER FOUND.", 30)
end
RotorOpsPerks.registerCtldCallbacks()
-- start a 5 second timer to monitor players, to allow other scripts to load