Improve Tradeships.lua
- Preparations: Add method Ship:GetCargo Add method Ship:GetDurationForDistance Add method Ship:Create Add method ui.treeNode & ui.treePop Add method LuaObject::CreateInLua Add functions for convenient selection of a random array element Add some numerical utilites Add a tool to easily draw a table from an array Fix ui.Format.Number Turn off the output when the ship changes frames Reduce the detection range of hyperspace clouds Remove the call LuaEvent::Emit immediately after exiting hyperspace Split Tradeships.lua into 5 modules - Tradeships For current system, calculate all possible routes of tradeships, durations, flows; arrange ships in space in such equilibrium state, as if the have been flying about their business for a long time. Calculate the average spawn interval for new ships, and parking intervals, so that the stations are not overcrowded or empty. Сreate a tab in the debug window with full information on routes, ships, stations, remote systems.master
parent
31911813f8
commit
20551ae2ea
|
@ -292,6 +292,35 @@ Ship.RemoveEquip = function (self, item, count, slot)
|
|||
return ret
|
||||
end
|
||||
|
||||
--
|
||||
-- Method: GetCargo
|
||||
--
|
||||
-- Build a table that maps the names of stored cargo items to the quantity of
|
||||
-- the items stored in the ship.
|
||||
--
|
||||
-- > cargo = ship:GetCargo()
|
||||
--
|
||||
-- Return:
|
||||
--
|
||||
-- The table that maps the names of stored cargo items to the quantity of
|
||||
-- the items stored in the ship.
|
||||
--
|
||||
-- Availability:
|
||||
--
|
||||
-- 2021
|
||||
--
|
||||
-- Status:
|
||||
--
|
||||
-- experimental
|
||||
--
|
||||
function Ship:GetCargo()
|
||||
local count = {}
|
||||
for _, et in pairs(self:GetEquip("cargo")) do
|
||||
if not count[et] then count[et] = 0 end
|
||||
count[et] = count[et]+1
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
--
|
||||
-- Method: IsHyperjumpAllowed
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
-- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt
|
||||
|
||||
local utils
|
||||
local Engine = require 'Engine' -- rand
|
||||
utils = {}
|
||||
|
||||
--
|
||||
|
@ -244,7 +245,7 @@ end
|
|||
-- Count the number of entries in a table
|
||||
utils.count = function(t)
|
||||
local i = 0
|
||||
for _,v in pairs(t) do
|
||||
for _,_ in pairs(t) do
|
||||
i = i + 1
|
||||
end
|
||||
return i
|
||||
|
@ -285,4 +286,149 @@ utils.round = function(x, n)
|
|||
x = math.round(math.abs(x)/n)*n
|
||||
return x < n and n*s or x*s
|
||||
end
|
||||
|
||||
--
|
||||
-- Function: utils.deviation
|
||||
--
|
||||
-- Returns a random value that differs from nominal by no more than nominal * ratio.
|
||||
--
|
||||
-- value = utils.deviation(nominal, ratio)
|
||||
--
|
||||
-- Return:
|
||||
--
|
||||
-- value - number
|
||||
--
|
||||
-- Parameters:
|
||||
--
|
||||
-- nominal - number
|
||||
-- ratio - number, indicating the relative deviation of the result
|
||||
--
|
||||
-- Example:
|
||||
--
|
||||
-- > value = utils.deviation(100, 0.2) -- 80 < value < 120
|
||||
--
|
||||
utils.deviation = function(nominal, ratio)
|
||||
return nominal * Engine.rand:Number(1 - ratio, 1 + ratio)
|
||||
end
|
||||
|
||||
--
|
||||
-- Function: utils.asymptote
|
||||
--
|
||||
-- The function is used to limit the value of the argument, but softly.
|
||||
-- See the desctription of the arguments.
|
||||
--
|
||||
-- value = utils.asymptote(x, max_value, equal_to_ratio)
|
||||
--
|
||||
-- Return:
|
||||
--
|
||||
-- value - number
|
||||
--
|
||||
-- Parameters:
|
||||
--
|
||||
-- x - number
|
||||
-- max_value - the return value will never be greater than this number, it
|
||||
-- will asymptotically approach it
|
||||
-- equal_to_ratio - 0.0 .. 1.0, the ratio between x and max_value up to which x is returned as is
|
||||
--
|
||||
-- Example:
|
||||
--
|
||||
-- > value = utils.asymptote(10, 100, 0.5) -- return 10
|
||||
-- > value = utils.asymptote(70, 100, 0.5) -- return 64.285
|
||||
-- > value = utils.asymptote(700, 100, 0.5) -- return 96.428
|
||||
-- > value = utils.asymptote(70000, 100, 0.5) -- return 99.964
|
||||
--
|
||||
utils.asymptote = function(x, max_value, equal_to_ratio)
|
||||
local equal_to = max_value * equal_to_ratio
|
||||
local equal_from = max_value - equal_to
|
||||
if x < equal_to then
|
||||
return x
|
||||
else
|
||||
return (1 - 1 / ((x - equal_to) / equal_from + 1)) * equal_from + equal_to
|
||||
end
|
||||
end
|
||||
|
||||
--
|
||||
-- Function: utils.normWeights
|
||||
--
|
||||
-- the input is an array of hashtables with an arbitrary real number in the
|
||||
-- weight key. Weights are recalculated so that the sum of the weights in the
|
||||
-- entire array equals 1 in fact, now these are the probabilities of selecting
|
||||
-- an item in the array
|
||||
--
|
||||
-- Example:
|
||||
--
|
||||
-- > utils.normWeights({ {param = 10, weight = 3.4},
|
||||
-- > {param = 15, weight = 2.1} })
|
||||
--
|
||||
-- Parameters:
|
||||
--
|
||||
-- array - an array of similar hashtables with an arbitrary real number in
|
||||
-- the weight key
|
||||
--
|
||||
-- Returns:
|
||||
--
|
||||
-- nothing
|
||||
--
|
||||
utils.normWeights = function(array)
|
||||
local sum = 0
|
||||
for _,v in ipairs(array) do
|
||||
sum = sum + v.weight
|
||||
end
|
||||
for _,v in ipairs(array) do
|
||||
v.weight = v.weight / sum
|
||||
end
|
||||
end
|
||||
|
||||
--
|
||||
-- Function: utils.chooseNormalized
|
||||
--
|
||||
-- Choose random item, considering the weights (probabilities).
|
||||
-- Each array[i] should have 'weight' key.
|
||||
-- The sum of the weights must be equal to 1.
|
||||
--
|
||||
-- Example:
|
||||
--
|
||||
-- > my_param = utils.chooseNormalized({ {param = 10, weight = 0.62},
|
||||
-- > {param = 15, weight = 0.38} }).param
|
||||
--
|
||||
-- Parameters:
|
||||
--
|
||||
-- array - an array of hashtables with an arbitrary real number in
|
||||
-- the weight key
|
||||
--
|
||||
-- Returns:
|
||||
--
|
||||
-- a random element of the array, with the probability specified in the weight key
|
||||
--
|
||||
utils.chooseNormalized = function(array)
|
||||
local choice = Engine.rand:Number(1.0)
|
||||
sum = 0
|
||||
for _, option in ipairs(array) do
|
||||
sum = sum + option.weight
|
||||
if choice <= sum then return option end
|
||||
end
|
||||
end
|
||||
|
||||
--
|
||||
-- Function: utils.chooseEqual
|
||||
--
|
||||
-- Returns a random element of an array
|
||||
--
|
||||
-- Example:
|
||||
--
|
||||
-- > my_param = utils.chooseEqual({ {param = 10},
|
||||
-- > {param = 15} }).param
|
||||
--
|
||||
-- Parameters:
|
||||
--
|
||||
-- array - an array of hashtables
|
||||
--
|
||||
-- Returns:
|
||||
--
|
||||
-- a random element of the array, with the with equal probability for any element
|
||||
--
|
||||
utils.chooseEqual = function(array)
|
||||
return array[Engine.rand:Integer(1, #array)]
|
||||
end
|
||||
|
||||
return utils
|
||||
|
|
|
@ -1,988 +0,0 @@
|
|||
-- Copyright © 2008-2021 Pioneer Developers. See AUTHORS.txt for details
|
||||
-- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt
|
||||
|
||||
local Engine = require 'Engine'
|
||||
local Game = require 'Game'
|
||||
local Space = require 'Space'
|
||||
local Comms = require 'Comms'
|
||||
local Timer = require 'Timer'
|
||||
local Event = require 'Event'
|
||||
local Serializer = require 'Serializer'
|
||||
local ShipDef = require 'ShipDef'
|
||||
local Ship = require 'Ship'
|
||||
local utils = require 'utils'
|
||||
local e = require 'Equipment'
|
||||
|
||||
--[[
|
||||
trade_ships
|
||||
interval - is minimum amount of time between hyperspace arrivals,
|
||||
stored here as it needs to be saved; number as seconds, updated by
|
||||
spawnInitialShips
|
||||
ship - object returned from Space:SpawnShip*
|
||||
ship_name - of this ship type; string
|
||||
ATMOSHIELD - flag indicating whether the ship has at atmospheric shield: boolean
|
||||
starport - at which this ship intends to dock; SpaceStation object
|
||||
dest_time - arrival time from hyperspace; number as Game.time
|
||||
dest_path - for hyperspace; SystemPath object, may have body index
|
||||
from_path - for hyperspace; SystemPath object
|
||||
delay - indicates waiting for a Timer to give next action; number
|
||||
as Game.time
|
||||
status - of this ship; string, one of:
|
||||
hyperspace - yet to arrive or has departed
|
||||
inbound - in system and given AIDockWith order
|
||||
docked - currently docked or un/docking
|
||||
outbound - heading away from starport before hyperspacing
|
||||
fleeing - has been attacked and is trying to get away
|
||||
(currently still just following whatever AI order it had)
|
||||
cowering - docked after having been attacked, waiting for
|
||||
attacker to go away
|
||||
orbit - was unable to dock, heading to or waiting in orbit
|
||||
cargo - table of cargo types and amounts currently carried;
|
||||
key: Constants.EquipType string, value: number
|
||||
attacker - what this was last attacked by; Body object
|
||||
chance - used to determine what action to take when attacked; number
|
||||
last_flee - when last action was taken, number as Game.time
|
||||
no_jump - whether this has tried to hyperspace away so it only
|
||||
tries once; bool
|
||||
|
||||
system_updated - indicates whether the following tables have been updated
|
||||
for the current system; bool, see onEnterSystem, onLeaveSystem, and
|
||||
onGameStart
|
||||
|
||||
from_paths - paths of systems around the current system, used to get a
|
||||
from_system for ships spawned in hyperspace; indexed array of
|
||||
SystemPath objects, updated by spawnInitialShips
|
||||
|
||||
starports - in the current system; indexed array of SpaceStation objects,
|
||||
updated by spawnInitialShips
|
||||
|
||||
vacuum_starports - in the current system; indexed array of SpaceStation objects that can be
|
||||
approached without atmospheric shields, updated by spawnInitialShips
|
||||
|
||||
imports, exports - in the current system, indexed array of
|
||||
equipment objects (from the 'Equipment' module), updated by spawnInitialShips
|
||||
--]]
|
||||
local trade_ships, system_updated, from_paths, starports, vacuum_starports, imports, exports
|
||||
|
||||
local addFuel = function (ship)
|
||||
local drive = ship:GetEquip('engine', 1)
|
||||
|
||||
-- a drive must be installed
|
||||
if not drive then
|
||||
print(trade_ships[ship]['ship_name']..' has no drive!')
|
||||
return nil
|
||||
end
|
||||
|
||||
-- the last character of the fitted drive is the class
|
||||
-- the fuel needed for max range is the square of the drive class
|
||||
local count = drive.capabilities.hyperclass ^ 2
|
||||
|
||||
-- account for fuel it already has
|
||||
count = count - ship:CountEquip(e.cargo.hydrogen)
|
||||
|
||||
local added = ship:AddEquip(e.cargo.hydrogen, count)
|
||||
|
||||
return added
|
||||
end
|
||||
|
||||
local addShipEquip = function (ship)
|
||||
local trader = trade_ships[ship]
|
||||
local ship_type = ShipDef[trader.ship_name]
|
||||
|
||||
-- add standard equipment
|
||||
ship:AddEquip(e.hyperspace['hyperdrive_'..tostring(ship_type.hyperdriveClass)])
|
||||
if ShipDef[ship.shipId].equipSlotCapacity.atmo_shield > 0 then
|
||||
ship:AddEquip(e.misc.atmospheric_shielding)
|
||||
trader.ATMOSHIELD = true -- flag this to save function calls later
|
||||
else
|
||||
-- This ship cannot safely land on a planet with an atmosphere.
|
||||
trader.ATMOSHIELD = false
|
||||
end
|
||||
ship:AddEquip(e.misc.radar)
|
||||
ship:AddEquip(e.misc.autopilot)
|
||||
ship:AddEquip(e.misc.cargo_life_support)
|
||||
|
||||
-- add defensive equipment based on lawlessness, luck and size
|
||||
local lawlessness = Game.system.lawlessness
|
||||
local size_factor = ship.freeCapacity ^ 2 / 2000000
|
||||
|
||||
if Engine.rand:Number(1) - 0.1 < lawlessness then
|
||||
local num = math.floor(math.sqrt(ship.freeCapacity / 50)) -
|
||||
ship:CountEquip(e.misc.shield_generator)
|
||||
if num > 0 then ship:AddEquip(e.misc.shield_generator, num) end
|
||||
if ship_type.equipSlotCapacity.energy_booster > 0 and
|
||||
Engine.rand:Number(1) + 0.5 - size_factor < lawlessness then
|
||||
ship:AddEquip(e.misc.shield_energy_booster)
|
||||
end
|
||||
end
|
||||
|
||||
-- we can't use these yet
|
||||
if ship_type.equipSlotCapacity.ecm > 0 then
|
||||
if Engine.rand:Number(1) + 0.2 < lawlessness then
|
||||
ship:AddEquip(e.misc.ecm_advanced)
|
||||
elseif Engine.rand:Number(1) < lawlessness then
|
||||
ship:AddEquip(e.misc.ecm_basic)
|
||||
end
|
||||
end
|
||||
|
||||
-- this should be rare
|
||||
if ship_type.equipSlotCapacity.hull_autorepair > 0 and
|
||||
Engine.rand:Number(1) + 0.75 - size_factor < lawlessness then
|
||||
ship:AddEquip(e.misc.hull_autorepair)
|
||||
end
|
||||
end
|
||||
|
||||
local addShipCargo = function (ship, direction)
|
||||
local total = 0
|
||||
local empty_space = math.min(ship.freeCapacity, ship:GetEquipFree("cargo"))
|
||||
local size_factor = empty_space / 20
|
||||
local ship_cargo = {}
|
||||
|
||||
if direction == 'import' and #imports == 1 then
|
||||
total = ship:AddEquip(imports[1], empty_space)
|
||||
ship_cargo[imports[1]] = total
|
||||
elseif direction == 'export' and #exports == 1 then
|
||||
total = ship:AddEquip(exports[1], empty_space)
|
||||
ship_cargo[exports[1]] = total
|
||||
elseif (direction == 'import' and #imports > 1) or
|
||||
(direction == 'export' and #exports > 1) then
|
||||
|
||||
-- happens if there was very little space left to begin with (eg small
|
||||
-- ship with lots of equipment). if we let it through then we end up
|
||||
-- trying to add 0 cargo forever
|
||||
if size_factor < 1 then
|
||||
trade_ships[ship]['cargo'] = ship_cargo
|
||||
return 0
|
||||
end
|
||||
|
||||
while total < empty_space do
|
||||
local cargo_type
|
||||
|
||||
-- get random for direction
|
||||
if direction == 'import' then
|
||||
cargo_type = imports[Engine.rand:Integer(1, #imports)]
|
||||
else
|
||||
cargo_type = exports[Engine.rand:Integer(1, #exports)]
|
||||
end
|
||||
|
||||
-- amount based on price and size of ship
|
||||
local num = math.abs(Game.system:GetCommodityBasePriceAlterations(cargo_type.name)) * size_factor
|
||||
num = Engine.rand:Integer(num, num * 2)
|
||||
|
||||
local added = ship:AddEquip(cargo_type, num)
|
||||
if ship_cargo[cargo_type] == nil then
|
||||
ship_cargo[cargo_type] = added
|
||||
else
|
||||
ship_cargo[cargo_type] = ship_cargo[cargo_type] + added
|
||||
end
|
||||
total = total + added
|
||||
end
|
||||
end
|
||||
-- if the table for direction was empty then cargo is empty and total is 0
|
||||
|
||||
trade_ships[ship]['cargo'] = ship_cargo
|
||||
return total
|
||||
end
|
||||
|
||||
local doUndock
|
||||
doUndock = function (ship)
|
||||
-- the player may have left the system or the ship may have already undocked
|
||||
if ship:exists() and ship:GetDockedWith() then
|
||||
local trader = trade_ships[ship]
|
||||
if not ship:Undock() then
|
||||
-- unable to undock, try again in ten minutes
|
||||
trader['delay'] = Game.time + 600
|
||||
Timer:CallAt(trader.delay, function () doUndock(ship) end)
|
||||
else
|
||||
trader['delay'] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local doOrbit = function (ship)
|
||||
local trader = trade_ships[ship]
|
||||
local sbody = trader.starport.path:GetSystemBody()
|
||||
local body = Space.GetBody(sbody.parent.index)
|
||||
ship:AIEnterLowOrbit(body)
|
||||
trader['status'] = 'orbit'
|
||||
print(ship.label..' ordering orbit of '..body.label)
|
||||
end
|
||||
|
||||
local getNearestStarport = function (ship, current)
|
||||
if #starports == 0 then return nil end
|
||||
|
||||
local trader = trade_ships[ship]
|
||||
|
||||
-- Find the nearest starport that we can land at (other than current)
|
||||
local starport, distance
|
||||
for i = 1, #starports do
|
||||
local next_starport = starports[i]
|
||||
if next_starport ~= current then
|
||||
local next_distance = ship:DistanceTo(next_starport)
|
||||
local next_canland = (trader.ATMOSHIELD or
|
||||
(next_starport.type == 'STARPORT_ORBITAL') or
|
||||
(not next_starport.path:GetSystemBody().parent.hasAtmosphere))
|
||||
|
||||
if next_canland and ((starport == nil) or (next_distance < distance)) then
|
||||
starport, distance = next_starport, next_distance
|
||||
end
|
||||
end
|
||||
end
|
||||
return starport or current
|
||||
end
|
||||
|
||||
local getSystem = function (ship)
|
||||
local max_range = ship.hyperspaceRange;
|
||||
if max_range > 30 then
|
||||
max_range = 30
|
||||
end
|
||||
local min_range = max_range / 2;
|
||||
if min_range < 7.5 then
|
||||
min_range = 7.5
|
||||
end
|
||||
local systems_in_range = Game.system:GetNearbySystems(min_range)
|
||||
if #systems_in_range == 0 then
|
||||
systems_in_range = Game.system:GetNearbySystems(max_range)
|
||||
end
|
||||
if #systems_in_range == 0 then return nil end
|
||||
if #systems_in_range == 1 then
|
||||
return systems_in_range[1].path
|
||||
end
|
||||
|
||||
local target_system = nil
|
||||
local best_prices = 0
|
||||
|
||||
-- find best system for cargo
|
||||
for _, next_system in ipairs(systems_in_range) do
|
||||
if #next_system:GetStationPaths() > 0 then
|
||||
local next_prices = 0
|
||||
for cargo, count in pairs(trade_ships[ship]['cargo']) do
|
||||
next_prices = next_prices + (next_system:GetCommodityBasePriceAlterations(cargo.name) * count)
|
||||
end
|
||||
if next_prices > best_prices then
|
||||
target_system, best_prices = next_system, next_prices
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if target_system == nil then
|
||||
-- pick a random system as fallback
|
||||
target_system = systems_in_range[Engine.rand:Integer(1, #systems_in_range)]
|
||||
|
||||
-- get closer systems
|
||||
local systems_half_range = Game.system:GetNearbySystems(min_range)
|
||||
|
||||
if #systems_half_range > 1 then
|
||||
target_system = systems_half_range[Engine.rand:Integer(1, #systems_half_range)]
|
||||
end
|
||||
end
|
||||
|
||||
-- pick a random starport, if there are any, so the game can simulate
|
||||
-- travel to it if player arrives after (see Space::DoHyperspaceTo)
|
||||
local target_starport_paths = target_system:GetStationPaths()
|
||||
if #target_starport_paths > 0 then
|
||||
return target_starport_paths[Engine.rand:Integer(1, #target_starport_paths)]
|
||||
end
|
||||
|
||||
return target_system.path
|
||||
end
|
||||
|
||||
local jumpToSystem = function (ship, target_path)
|
||||
if target_path == nil then return nil end
|
||||
|
||||
local status, fuel, duration = ship:HyperjumpTo(target_path)
|
||||
|
||||
if status ~= 'OK' then
|
||||
print(trade_ships[ship]['ship_name']..' jump status is not OK')
|
||||
return status
|
||||
end
|
||||
|
||||
-- update table for ship
|
||||
trade_ships[ship]['status'] = 'hyperspace'
|
||||
trade_ships[ship]['starport'] = nil
|
||||
trade_ships[ship]['dest_time'] = Game.time + duration
|
||||
trade_ships[ship]['dest_path'] = target_path
|
||||
trade_ships[ship]['from_path'] = Game.system.path
|
||||
return status
|
||||
end
|
||||
|
||||
local getSystemAndJump = function (ship)
|
||||
-- attention all coders: trade_ships[ship].starport may be nil
|
||||
if trade_ships[ship].starport then
|
||||
local body = Space.GetBody(trade_ships[ship].starport.path:GetSystemBody().parent.index)
|
||||
local port = trade_ships[ship].starport
|
||||
-- boost away from the starport before jumping if it is too close
|
||||
if (ship:DistanceTo(port) < 20000) then
|
||||
ship:AIEnterLowOrbit(body)
|
||||
end
|
||||
return jumpToSystem(ship, getSystem(ship))
|
||||
end
|
||||
end
|
||||
|
||||
local getAcceptableShips = function ()
|
||||
-- only accept ships with enough capacity that are capable of landing in atmospheres
|
||||
local filter_function
|
||||
if #vacuum_starports == 0 then
|
||||
filter_function = function(k,def)
|
||||
-- XXX should limit to ships large enough to carry significant
|
||||
-- cargo, but we don't have enough ships yet
|
||||
return def.tag == 'SHIP' and def.hyperdriveClass > 0 and def.equipSlotCapacity.atmo_shield > 0
|
||||
end
|
||||
else
|
||||
filter_function = function(k,def)
|
||||
-- XXX should limit to ships large enough to carry significant
|
||||
-- cargo, but we don't have enough ships yet
|
||||
return def.tag == 'SHIP' and def.hyperdriveClass > 0
|
||||
end
|
||||
end
|
||||
return utils.build_array(
|
||||
utils.map(function (k,def)
|
||||
return k,def.id
|
||||
end,
|
||||
utils.filter(filter_function,
|
||||
pairs(ShipDef)
|
||||
)))
|
||||
end
|
||||
|
||||
local spawnInitialShips = function (game_start)
|
||||
-- quicker checks first
|
||||
-- dont spawn tradeships in unpopulated systems
|
||||
local population = Game.system.population
|
||||
if population == 0 then
|
||||
starports = {}
|
||||
return nil
|
||||
end
|
||||
|
||||
-- check if the current system can be traded in
|
||||
starports = Space.GetBodies(function (body) return body.superType == 'STARPORT' end)
|
||||
if #starports == 0 then return nil end
|
||||
vacuum_starports = Space.GetBodies(function (body)
|
||||
return body.superType == 'STARPORT' and (body.type == 'STARPORT_ORBITAL' or (not body.path:GetSystemBody().parent.hasAtmosphere))
|
||||
end)
|
||||
|
||||
-- get ships listed as tradeships, if none - give up
|
||||
local ship_names = getAcceptableShips()
|
||||
if #ship_names == 0 then return nil end
|
||||
|
||||
-- get a measure of the market size and build lists of imports and exports
|
||||
local import_score, export_score = 0, 0
|
||||
imports, exports = {}, {}
|
||||
for key, equip in pairs(e.cargo) do
|
||||
local v = Game.system:GetCommodityBasePriceAlterations(key)
|
||||
if key ~= "rubbish" and key ~= "radioactives" and Game.system:IsCommodityLegal(key) then
|
||||
-- values from SystemInfoView::UpdateEconomyTab
|
||||
|
||||
if v > 4 then
|
||||
import_score = import_score + (v > 10 and 2 or 1) -- lua is crazy
|
||||
table.insert(imports, equip)
|
||||
end
|
||||
|
||||
if v < -4 then
|
||||
export_score = export_score + (v < -10 and 2 or 1)
|
||||
table.insert(exports, equip)
|
||||
end
|
||||
end
|
||||
end
|
||||
-- if there is no market then there is no trade
|
||||
if #imports == 0 or #exports == 0 then return nil end
|
||||
|
||||
-- determine how many trade ships to spawn
|
||||
local lawlessness = Game.system.lawlessness
|
||||
-- start with three ships per two billion population
|
||||
local num_trade_ships = population * 1.5
|
||||
-- add the average of import_score and export_score
|
||||
num_trade_ships = num_trade_ships + (import_score + export_score) / 2
|
||||
-- reduce based on lawlessness
|
||||
num_trade_ships = num_trade_ships * (1 - lawlessness)
|
||||
-- vary by up to twice as many with a bell curve probability
|
||||
num_trade_ships = num_trade_ships * (Engine.rand:Number(0.25, 1) + Engine.rand:Number(0.25, 1))
|
||||
-- compute distance and interval between ships
|
||||
-- the base number of AU between ships spawned in space
|
||||
local range = (9 / (num_trade_ships * 0.75))
|
||||
if game_start then
|
||||
range = range * 1.5
|
||||
end
|
||||
-- the base number of seconds between ships spawned in hyperspace
|
||||
trade_ships['interval'] = (864000 / (num_trade_ships / 4))
|
||||
-- get nearby system paths for hyperspace spawns to come from
|
||||
local from_systems, dist = {}, 10
|
||||
while #from_systems < 10 do
|
||||
from_systems = Game.system:GetNearbySystems(dist)
|
||||
dist = dist + 5
|
||||
end
|
||||
from_paths = {}
|
||||
for _, system in ipairs(from_systems) do
|
||||
table.insert(from_paths, system.path)
|
||||
end
|
||||
|
||||
-- spawn the initial trade ships
|
||||
for i = 0, num_trade_ships do
|
||||
-- get the name of a ship, for example 'imperial_courier'
|
||||
local ship_name = ship_names[Engine.rand:Integer(1, #ship_names)]
|
||||
local can_equip_atmo = ShipDef[ship_name].equipSlotCapacity.atmo_shield > 0
|
||||
local ship = nil
|
||||
|
||||
if game_start and i < num_trade_ships / 4 then
|
||||
-- spawn the first quarter in port if at game start
|
||||
local starport = nil
|
||||
if can_equip_atmo then
|
||||
starport = starports[Engine.rand:Integer(1, #starports)]
|
||||
elseif #vacuum_starports then
|
||||
starport = vacuum_starports[Engine.rand:Integer(1, #vacuum_starports)]
|
||||
end
|
||||
if starport then
|
||||
local dockstatus = 'docked'
|
||||
ship = Space.SpawnShipDocked(ship_name, starport)
|
||||
if ship == nil then
|
||||
-- the starport must have been full
|
||||
ship = Space.SpawnShipNear(ship_name, starport, 10000000, 149598000) -- 10mkm - 1AU
|
||||
dockstatus = 'inbound'
|
||||
end
|
||||
trade_ships[ship] = { status = dockstatus, starport = starport, ship_name = ship_name }
|
||||
ship:SetLabel(Ship.MakeRandomLabel())
|
||||
addShipEquip(ship)
|
||||
end
|
||||
elseif i < num_trade_ships * 0.75 then
|
||||
-- spawn the first three quarters in space, or middle half if game start
|
||||
local min_dist = range * i + 1
|
||||
if game_start then
|
||||
min_dist = min_dist - (range * (num_trade_ships / 4))
|
||||
end
|
||||
|
||||
ship = Space.SpawnShip(ship_name, min_dist, min_dist + range)
|
||||
ship:SetLabel(Ship.MakeRandomLabel())
|
||||
trade_ships[ship] = { status = 'inbound', ship_name = ship_name }
|
||||
-- Add ship equipment right now, because...
|
||||
addShipEquip(ship)
|
||||
-- ...this next call needs to see if there's an atmospheric shield.
|
||||
trade_ships[ship].starport = getNearestStarport(ship)
|
||||
else
|
||||
-- spawn the last quarter in hyperspace
|
||||
local min_time = trade_ships.interval * (i - num_trade_ships * 0.75)
|
||||
local max_time = min_time + trade_ships.interval
|
||||
local dest_time = Game.time + Engine.rand:Integer(min_time, max_time)
|
||||
local from = from_paths[Engine.rand:Integer(1, #from_paths)]
|
||||
|
||||
ship = Space.SpawnShip(ship_name, 9, 11, {from, dest_time})
|
||||
ship:SetLabel(Ship.MakeRandomLabel())
|
||||
trade_ships[ship] = {
|
||||
status = 'hyperspace',
|
||||
dest_time = dest_time,
|
||||
dest_path = Game.system.path,
|
||||
from_path = from,
|
||||
ship_name = ship_name,
|
||||
}
|
||||
addShipEquip(ship)
|
||||
end
|
||||
if ship then
|
||||
local trader = trade_ships[ship]
|
||||
|
||||
-- add cargo
|
||||
local fuel_added = addFuel(ship)
|
||||
if trader.status == 'docked' then
|
||||
local delay = fuel_added + addShipCargo(ship, 'export')
|
||||
-- have ship wait 30-45 seconds per unit of cargo
|
||||
if delay > 0 then
|
||||
trader['delay'] = Game.time + (delay * Engine.rand:Number(30, 45))
|
||||
else
|
||||
trader['delay'] = Game.time + Engine.rand:Number(600, 3600)
|
||||
end
|
||||
Timer:CallAt(trader.delay, function () doUndock(ship) end)
|
||||
else
|
||||
addShipCargo(ship, 'import')
|
||||
-- remove fuel used to get here
|
||||
if fuel_added and fuel_added > 0 then
|
||||
ship:RemoveEquip(e.cargo.hydrogen, Engine.rand:Integer(1, fuel_added))
|
||||
end
|
||||
if trader.status == 'inbound' then
|
||||
ship:AIDockWith(trader.starport)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return num_trade_ships
|
||||
end
|
||||
|
||||
local spawnReplacement = function ()
|
||||
-- spawn new ship in hyperspace
|
||||
if #starports > 0 and Game.system.population > 0 and #imports > 0 and #exports > 0 then
|
||||
local ship_names = getAcceptableShips()
|
||||
local ship_name = ship_names[Engine.rand:Integer(1, #ship_names)]
|
||||
|
||||
local dest_time = Game.time + Engine.rand:Number(trade_ships.interval, trade_ships.interval * 2)
|
||||
local from = from_paths[Engine.rand:Integer(1, #from_paths)]
|
||||
|
||||
local ship = Space.SpawnShip(ship_name, 9, 11, {from, dest_time})
|
||||
ship:SetLabel(Ship.MakeRandomLabel())
|
||||
trade_ships[ship] = {
|
||||
status = 'hyperspace',
|
||||
dest_time = dest_time,
|
||||
dest_path = Game.system.path,
|
||||
from_path = from,
|
||||
ship_name = ship_name,
|
||||
}
|
||||
|
||||
addShipEquip(ship)
|
||||
local fuel_added = addFuel(ship)
|
||||
addShipCargo(ship, 'import')
|
||||
if fuel_added and fuel_added > 0 then
|
||||
ship:RemoveEquip(e.cargo.hydrogen, Engine.rand:Integer(1, fuel_added))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local updateTradeShipsTable = function ()
|
||||
local total, removed = 0, 0
|
||||
for ship, trader in pairs(trade_ships) do
|
||||
total = total + 1
|
||||
if trader.status == 'hyperspace' then
|
||||
-- remove ships not coming here
|
||||
if not trader.dest_path:IsSameSystem(Game.system.path) then
|
||||
trade_ships[ship] = nil
|
||||
removed = removed + 1
|
||||
end
|
||||
else
|
||||
-- remove ships that are not in hyperspace
|
||||
trade_ships[ship] = nil
|
||||
removed = removed + 1
|
||||
end
|
||||
end
|
||||
print('updateTSTable:total:'..total..',removed:'..removed)
|
||||
end
|
||||
|
||||
local cleanTradeShipsTable = function ()
|
||||
local total, hyperspace, removed = 0, 0, 0
|
||||
for ship, trader in pairs(trade_ships) do
|
||||
if ship ~= 'interval' then
|
||||
total = total + 1
|
||||
if trader.status == 'hyperspace' then
|
||||
hyperspace = hyperspace + 1
|
||||
-- remove well past due ships as the player can not catch them
|
||||
if trader.dest_time + 86400 < Game.time then
|
||||
trade_ships[ship] = nil
|
||||
removed = removed + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
print('cleanTSTable:total:'..total..',active:'..total - hyperspace..',removed:'..removed)
|
||||
end
|
||||
|
||||
local onEnterSystem = function (ship)
|
||||
-- dont crash when entering unexplored systems
|
||||
if Game.system.explored == false then
|
||||
return
|
||||
end
|
||||
|
||||
-- if the player is following a ship through hyperspace that ship may enter first
|
||||
-- so update the system when the first ship enters (see Space::DoHyperspaceTo)
|
||||
if not system_updated then
|
||||
updateTradeShipsTable()
|
||||
spawnInitialShips(false)
|
||||
system_updated = true
|
||||
end
|
||||
|
||||
if trade_ships[ship] ~= nil then
|
||||
local trader = trade_ships[ship]
|
||||
print(ship.label..' '..trader.ship_name..' entered '..Game.system.name..' from '..trader.from_path:GetStarSystem().name)
|
||||
|
||||
local starport = getNearestStarport(ship)
|
||||
if starport then
|
||||
ship:AIDockWith(starport)
|
||||
trade_ships[ship]['starport'] = starport
|
||||
trade_ships[ship]['status'] = 'inbound'
|
||||
else
|
||||
-- starport == nil happens if player has followed ship to empty system, or
|
||||
-- no suitable port found (e.g. all stations atmospheric for ship without atmoshield)
|
||||
getSystemAndJump(ship)
|
||||
-- if we couldn't reach any systems wait for player to attack
|
||||
end
|
||||
end
|
||||
end
|
||||
Event.Register("onEnterSystem", onEnterSystem)
|
||||
|
||||
local onLeaveSystem = function (ship)
|
||||
if ship:IsPlayer() then
|
||||
-- the next onEnterSystem will be in a new system
|
||||
system_updated = false
|
||||
trade_ships['interval'] = nil
|
||||
|
||||
local total, removed = 0, 0
|
||||
for t_ship, trader in pairs(trade_ships) do
|
||||
total = total + 1
|
||||
if trader.status == 'hyperspace' then
|
||||
if trader.dest_path:IsSameSystem(Game.system.path) then
|
||||
-- remove ships that are in hyperspace to here
|
||||
trade_ships[t_ship] = nil
|
||||
removed = removed + 1
|
||||
end
|
||||
else
|
||||
-- remove all ships that are not in hyperspace
|
||||
trade_ships[t_ship] = nil
|
||||
removed = removed + 1
|
||||
end
|
||||
end
|
||||
print('onLeaveSystem:total:'..total..',removed:'..removed)
|
||||
elseif trade_ships[ship] ~= nil then
|
||||
local system = trade_ships[ship]['dest_path']:GetStarSystem()
|
||||
print(ship.label..' left '..Game.system.name..' for '..system.name)
|
||||
cleanTradeShipsTable()
|
||||
spawnReplacement()
|
||||
end
|
||||
end
|
||||
Event.Register("onLeaveSystem", onLeaveSystem)
|
||||
|
||||
local onFrameChanged = function (ship)
|
||||
if not ship:isa("Ship") or trade_ships[ship] == nil then return end
|
||||
local trader = trade_ships[ship]
|
||||
|
||||
if trader.status == 'outbound' then
|
||||
-- the cloud inherits the ship velocity and vector
|
||||
ship:CancelAI()
|
||||
if getSystemAndJump(ship) ~= 'OK' then
|
||||
ship:AIDockWith(trader.starport)
|
||||
trader['status'] = 'inbound'
|
||||
end
|
||||
end
|
||||
end
|
||||
Event.Register("onFrameChanged", onFrameChanged)
|
||||
|
||||
local onShipDocked = function (ship, starport)
|
||||
if trade_ships[ship] == nil then return end
|
||||
local trader = trade_ships[ship]
|
||||
|
||||
print(ship.label..' docked with '..starport.label..' ship:'..trader.ship_name)
|
||||
|
||||
if trader.status == 'fleeing' then
|
||||
trader['status'] = 'cowering'
|
||||
else
|
||||
trader['status'] = 'docked'
|
||||
end
|
||||
if trader.chance then
|
||||
trader['chance'] = trader.chance / 2
|
||||
trader['last_flee'], trader['no_jump'] = nil, nil
|
||||
end
|
||||
|
||||
-- 'sell' trade cargo
|
||||
local delay = 0
|
||||
for cargo, _ in pairs(trader.cargo) do
|
||||
delay = delay + ship:RemoveEquip(cargo, 1000000)
|
||||
end
|
||||
|
||||
local damage = ShipDef[trader.ship_name].hullMass -
|
||||
ship.hullMassLeft
|
||||
if damage > 0 then
|
||||
ship:SetHullPercent()
|
||||
addShipEquip(ship)
|
||||
damage = damage * 4
|
||||
end
|
||||
addFuel(ship)
|
||||
delay = delay + addShipCargo(ship, 'export')
|
||||
if damage > delay then delay = damage end
|
||||
|
||||
-- delay undocking by 30-45 seconds for every unit of cargo transfered
|
||||
-- or 2-3 minutes for every unit of hull repaired
|
||||
if delay > 0 then
|
||||
trader['delay'] = Game.time + (delay * Engine.rand:Number(30, 45))
|
||||
else
|
||||
trader['delay'] = Game.time + Engine.rand:Number(600, 3600)
|
||||
end
|
||||
|
||||
if trader.status == 'docked' then
|
||||
Timer:CallAt(trader.delay, function () doUndock(ship) end)
|
||||
end
|
||||
end
|
||||
Event.Register("onShipDocked", onShipDocked)
|
||||
|
||||
local onShipUndocked = function (ship, starport)
|
||||
if trade_ships[ship] == nil then return end
|
||||
|
||||
-- fly to the limit of the starport frame
|
||||
ship:AIFlyTo(starport)
|
||||
|
||||
trade_ships[ship]['status'] = 'outbound'
|
||||
end
|
||||
Event.Register("onShipUndocked", onShipUndocked)
|
||||
|
||||
local onAICompleted = function (ship, ai_error)
|
||||
if trade_ships[ship] == nil then return end
|
||||
local trader = trade_ships[ship]
|
||||
if ai_error ~= 'NONE' then
|
||||
print(ship.label..' AICompleted: Error: '..ai_error..' Status: '..trader.status) end
|
||||
|
||||
if trader.status == 'outbound' then
|
||||
if getSystemAndJump(ship) ~= 'OK' then
|
||||
ship:AIDockWith(trader.starport)
|
||||
trader['status'] = 'inbound'
|
||||
end
|
||||
elseif trader.status == 'orbit' then
|
||||
if ai_error == 'NONE' then
|
||||
trader['delay'] = Game.time + 21600 -- 6 hours
|
||||
Timer:CallAt(trader.delay, function ()
|
||||
if ship:exists() and ship.flightState ~= 'HYPERSPACE' then
|
||||
trader['starport'] = getNearestStarport(ship, trader.starport)
|
||||
ship:AIDockWith(trader.starport)
|
||||
trader['status'] = 'inbound'
|
||||
trader['delay'] = nil
|
||||
end
|
||||
end)
|
||||
end
|
||||
-- XXX if ORBIT_IMPOSSIBLE asteroid? get parent of parent and attempt orbit?
|
||||
elseif trader.status == 'inbound' then
|
||||
if ai_error == 'REFUSED_PERM' then doOrbit(ship) end
|
||||
end
|
||||
end
|
||||
Event.Register("onAICompleted", onAICompleted)
|
||||
|
||||
local onShipLanded = function (ship, body)
|
||||
if trade_ships[ship] == nil then return end
|
||||
print(ship.label..' Landed: '..trade_ships[ship].starport.label)
|
||||
|
||||
doOrbit(ship)
|
||||
end
|
||||
Event.Register("onShipLanded", onShipLanded)
|
||||
|
||||
local onShipAlertChanged = function (ship, alert)
|
||||
if trade_ships[ship] == nil then return end
|
||||
if alert == 'SHIP_FIRING' then
|
||||
print(ship.label..' alert changed to '..alert) end
|
||||
local trader = trade_ships[ship]
|
||||
if trader.attacker == nil then return end
|
||||
|
||||
if alert == 'NONE' or not trader.attacker:exists() or
|
||||
(alert == 'SHIP_NEARBY' and ship:DistanceTo(trader.attacker) > 100) then
|
||||
trader['attacker'] = nil
|
||||
if trader.status == 'fleeing' then
|
||||
-- had not reached starport yet
|
||||
trader['status'] = 'inbound'
|
||||
elseif trader.status == 'cowering' then
|
||||
-- already reached starport and docked
|
||||
trader['status'] = 'docked'
|
||||
if trader.delay > Game.time then
|
||||
--[[ not ready to undock, so schedule it
|
||||
there is a slight chance that the status was changed while
|
||||
onShipDocked was in progress so fire a bit later ]]
|
||||
Timer:CallAt(trader.delay + 120, function () doUndock(ship) end)
|
||||
else
|
||||
-- ready to undock
|
||||
doUndock(ship)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Event.Register("onShipAlertChanged", onShipAlertChanged)
|
||||
|
||||
local onShipHit = function (ship, attacker)
|
||||
if attacker == nil then return end-- XX
|
||||
|
||||
-- XXX this whole thing might be better if based on amount of damage sustained
|
||||
if trade_ships[ship] == nil then return end
|
||||
local trader = trade_ships[ship]
|
||||
|
||||
trader['chance'] = trader.chance or 0
|
||||
trader['chance'] = trader.chance + 0.1
|
||||
|
||||
-- don't spam actions
|
||||
if trader.last_flee and Game.time - trader.last_flee < Engine.rand:Integer(5, 7) then return end
|
||||
|
||||
-- if outbound jump now
|
||||
if trader.status == 'outbound' then
|
||||
if getSystemAndJump(ship) == 'OK' then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
trader['status'] = 'fleeing'
|
||||
trader['attacker'] = attacker
|
||||
|
||||
-- update last_flee
|
||||
trader['last_flee'] = Game.time
|
||||
|
||||
-- if distance to starport is far attempt to hyperspace
|
||||
if trader.no_jump ~= true then
|
||||
if #starports == 0 then
|
||||
trader['no_jump'] = true -- it already tried in onEnterSystem
|
||||
elseif trader.starport and Engine.rand:Number(1) < trader.chance then
|
||||
local distance = ship:DistanceTo(trader.starport)
|
||||
if distance > 149598000 * (2 - trader.chance) then -- 149,598,000km = 1AU
|
||||
if getSystemAndJump(ship) then
|
||||
return
|
||||
else
|
||||
trader['no_jump'] = true
|
||||
trader['chance'] = trader.chance + 0.3
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- maybe jettison a bit of cargo
|
||||
if Engine.rand:Number(1) < trader.chance then
|
||||
local cargo_type = nil
|
||||
local max_cap = ShipDef[ship.shipId].capacity
|
||||
for k, v in pairs(trader.cargo) do
|
||||
if v > 1 and Engine.rand:Number(1) < v / max_cap then
|
||||
cargo_type = k
|
||||
break
|
||||
end
|
||||
end
|
||||
if cargo_type and ship:Jettison(cargo_type) then
|
||||
trader.cargo[cargo_type] = trader.cargo[cargo_type] - 1
|
||||
Comms.ImportantMessage(attacker.label..', take this and leave us be, you filthy pirate!', ship.label)
|
||||
trader['chance'] = trader.chance - 0.1
|
||||
end
|
||||
end
|
||||
end
|
||||
Event.Register("onShipHit", onShipHit)
|
||||
|
||||
local onShipCollided = function (ship, other)
|
||||
if trade_ships[ship] == nil then return end
|
||||
if other:isa('CargoBody') then return end
|
||||
|
||||
if other:isa('Ship') and other:IsPlayer() then
|
||||
onShipHit(ship, other)
|
||||
return
|
||||
end
|
||||
|
||||
-- try to get away from body, onAICompleted will take over if we succeed
|
||||
ship:AIFlyTo(other)
|
||||
end
|
||||
Event.Register("onShipCollided", onShipCollided)
|
||||
|
||||
local onShipDestroyed = function (ship, attacker)
|
||||
if attacker == nil then return end-- XX
|
||||
if trade_ships[ship] ~= nil then
|
||||
local trader = trade_ships[ship]
|
||||
|
||||
print(ship.label..' destroyed by '..attacker.label..', status:'..trader.status..' ship:'..trader.ship_name..', starport:'..(trader.starport and trader.starport.label or 'N/A'))
|
||||
trade_ships[ship] = nil
|
||||
|
||||
if not attacker:isa("Ship") then
|
||||
spawnReplacement()
|
||||
end
|
||||
-- XXX consider spawning some CargoBodies if killed by a ship
|
||||
else
|
||||
for t_ship, trader in pairs(trade_ships) do
|
||||
if t_ship ~= 'interval' and trader.attacker and trader.attacker == ship then
|
||||
trader['attacker'] = nil
|
||||
if trader.status == 'fleeing' then
|
||||
-- had not reached starport yet
|
||||
trader['status'] = 'inbound'
|
||||
elseif trader.status == 'cowering' then
|
||||
-- already reached starport and docked
|
||||
trader['status'] = 'docked'
|
||||
|
||||
if trader.delay > Game.time then
|
||||
--[[ not ready to undock, so schedule it
|
||||
there is a slight chance that the status was changed while
|
||||
onShipDocked was in progress so fire a bit later ]]
|
||||
Timer:CallAt(trader.delay + 120, function () doUndock(t_ship) end)
|
||||
else
|
||||
-- ready to undock
|
||||
doUndock(t_ship)
|
||||
end
|
||||
end
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Event.Register("onShipDestroyed", onShipDestroyed)
|
||||
|
||||
local onGameStart = function ()
|
||||
-- create tables for data on the current system
|
||||
from_paths, starports, imports, exports = {}, {}, {}, {}
|
||||
|
||||
system_updated = true
|
||||
|
||||
if trade_ships == nil then
|
||||
-- create table to hold ships, keyed by ship object
|
||||
trade_ships = {}
|
||||
spawnInitialShips(true)
|
||||
else
|
||||
-- trade_ships was loaded by unserialize
|
||||
-- rebuild starports, imports and exports tables
|
||||
starports = Space.GetBodies(function (body) return body.superType == 'STARPORT' end)
|
||||
vacuum_starports = Space.GetBodies(function (body)
|
||||
return body.superType == 'STARPORT' and (body.type == 'STARPORT_ORBITAL' or (not body.path:GetSystemBody().parent.hasAtmosphere))
|
||||
end)
|
||||
if #starports == 0 then
|
||||
-- there are no starports so don't bother looking for goods
|
||||
return
|
||||
else
|
||||
for key,equip in pairs(e.cargo) do
|
||||
local v = Game.system:GetCommodityBasePriceAlterations(key)
|
||||
if key ~= 'rubbish' and key ~= 'radioactives' and Game.system:IsCommodityLegal(key) then
|
||||
if v > 4 then
|
||||
table.insert(imports, equip)
|
||||
elseif v < -4 then
|
||||
table.insert(exports, equip)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- rebuild nearby system paths for hyperspace spawns to come from
|
||||
local from_systems, dist = {}, 10
|
||||
while #from_systems < 10 do
|
||||
from_systems = Game.system:GetNearbySystems(dist)
|
||||
dist = dist + 5
|
||||
end
|
||||
from_paths = {}
|
||||
for _, system in ipairs(from_systems) do
|
||||
table.insert(from_paths, system.path)
|
||||
end
|
||||
|
||||
-- check if any trade ships were waiting on a timer
|
||||
for ship, trader in pairs(trade_ships) do
|
||||
if ship ~= 'interval' and trader.delay and trader.delay > Game.time then
|
||||
if trader.status == 'docked' then
|
||||
Timer:CallAt(trader.delay, function () doUndock(ship) end)
|
||||
elseif trader.status == 'orbit' then
|
||||
Timer:CallAt(trader.delay, function ()
|
||||
if ship:exists() and ship.flightState ~= 'HYPERSPACE' then
|
||||
trader['starport'] = getNearestStarport(ship)
|
||||
ship:AIDockWith(trader.starport)
|
||||
trader['status'] = 'inbound'
|
||||
trader['delay'] = nil
|
||||
end
|
||||
end)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Event.Register("onGameStart", onGameStart)
|
||||
|
||||
local onGameEnd = function ()
|
||||
-- drop the references for our data so Lua can free them
|
||||
-- and so we can start fresh if the player starts another game
|
||||
trade_ships, system_updated, from_paths, starports, vacuum_starports, imports, exports = nil, nil, nil, nil, nil, nil, nil
|
||||
end
|
||||
Event.Register("onGameEnd", onGameEnd)
|
||||
|
||||
local serialize = function ()
|
||||
-- all we need to save is trade_ships, the rest can be rebuilt on load
|
||||
|
||||
-- The serializer will crash if we try to serialize dead objects (issue #3123)
|
||||
-- also, trade_ships may be nil, because it is cleared in 'onGameEnd', and this may
|
||||
-- happen before the autosave module creates its '_exit' save
|
||||
if trade_ships ~= nil then
|
||||
local count = 0
|
||||
for k,v in pairs(trade_ships) do
|
||||
if type(k) == 'userdata' and not k:exists() then
|
||||
count = count + 1
|
||||
-- according to the Lua manual, removing items during iteration with pairs() or next() is ok
|
||||
trade_ships[k] = nil
|
||||
end
|
||||
end
|
||||
print('TradeShips: Removed ' .. count .. ' ships before serialization')
|
||||
end
|
||||
return trade_ships
|
||||
end
|
||||
|
||||
local unserialize = function (data)
|
||||
trade_ships = data
|
||||
end
|
||||
|
||||
Serializer:Register("TradeShips", serialize, unserialize)
|
|
@ -0,0 +1,200 @@
|
|||
-- Copyright © 2008-2021 Pioneer Developers. See AUTHORS.txt for details
|
||||
-- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt
|
||||
|
||||
local Game = require 'Game'
|
||||
local Serializer = require 'Serializer'
|
||||
|
||||
-- this module is responsible for storing global variables, logging, serialization
|
||||
local Core = {
|
||||
-- CONSTANTS / SETTINGS
|
||||
AU = 149598000000.0, -- meters
|
||||
|
||||
MAX_ROUTE_FLOW = 3, -- flow to the most popular station in the system, ships/hour
|
||||
MAX_BUSY = 0.4, -- maximum station load
|
||||
MAX_SHIPS = 500, -- ~ maximim ships in open space (limitation for performance reasons)
|
||||
MIN_STATION_DOCKING_TIME = 2, -- hours, minimum average time of the ship's stay at the station
|
||||
|
||||
WAIT_FOR_NEXT_UNDOCK = 600, -- seconds
|
||||
MINIMUM_NEAR_SYSTEMS = 10,
|
||||
LOG_SIZE = 2000, -- records in log
|
||||
|
||||
-- for debug info
|
||||
last_spawn_interval = nil,
|
||||
}
|
||||
|
||||
--[[
|
||||
Property: Core.ships
|
||||
(serializable table)
|
||||
<key> - object returned from Space:SpawnShip*
|
||||
<value>: hashtable
|
||||
ship_name - of this ship type; string
|
||||
starport - at which this ship intends to dock; SpaceStation object
|
||||
dest_time - arrival time from hyperspace; number as Game.kjtime
|
||||
dest_path - for hyperspace; SystemPath object, may have body index
|
||||
from_path - for hyperspace; SystemPath object
|
||||
delay - indicates waiting for a Timer to give next action; number as Game.time
|
||||
status - of this ship; string, one of:
|
||||
hyperspace - yet to arrive
|
||||
hyperspace_out - has departed
|
||||
inbound - in system and given AIDockWith order
|
||||
docked - currently docked or un/docking
|
||||
outbound - heading away from starport before hyperspacing
|
||||
fleeing - has been attacked and is trying to get away
|
||||
(currently still just following whatever AI order it had)
|
||||
cowering - docked after having been attacked, waiting for
|
||||
attacker to go away
|
||||
orbit - was unable to dock, heading to or waiting in orbit
|
||||
attacker: hashtable
|
||||
ref - what this was last attacked by; Body object
|
||||
chance - used to determine what action to take when attacked; number
|
||||
last_flee - when last action was taken, number as Game.time
|
||||
no_jump - whether this has tried to hyperspace away so it only tries once; bool
|
||||
--]]
|
||||
Core.ships = nil
|
||||
|
||||
--[[
|
||||
Property: Core.params
|
||||
(non-serializable table - generated every time, accorging to the current system)
|
||||
ship_names - list of ship names (shipId) that can actually trade in this system
|
||||
total_flow - ship flow from hyperspace, (ships per hour)
|
||||
local_routes - available routes lookup table by ship name
|
||||
<key> - shipId (see Ship.lua)
|
||||
<value>: hashtable
|
||||
from - body
|
||||
to - body
|
||||
distance - from -> to
|
||||
ndocks - total docks at destination port
|
||||
duration - seconds
|
||||
weight - in fact, the probability
|
||||
flow - of ships on the route (ship per hour)
|
||||
amount - average number of ships on the route
|
||||
hyper_routes - available hyperspace routes lookup table by ship name
|
||||
<key> - shipId (see Ship.lua)
|
||||
<value>: hashtable
|
||||
from - system path
|
||||
distance - l.y.
|
||||
fuel - jump fuel consumption
|
||||
duration - jump duration
|
||||
cloud_duration - how long before the exit from hyperspace does the cloud appear
|
||||
flow - of ships on the route (ship per hour)
|
||||
ships - average number of ships on the route
|
||||
port_params - station parameters lookup table by it's path
|
||||
<key> - body, spacestation
|
||||
<value>: hashtable
|
||||
flow - of ships to the port (ship per hour)
|
||||
ndocks - total docks at destination port
|
||||
landed - average number of ships landed
|
||||
busy - landed / docks
|
||||
time - average duration of a ship parted at a station
|
||||
imports, exports - in the current system, indexed array of
|
||||
equipment objects (from the 'Equipment' module),
|
||||
updated by spawnInitialShips
|
||||
--]]
|
||||
Core.params = nil
|
||||
|
||||
-- circular buffer for log messages
|
||||
Core.log = {
|
||||
clear = function(self)
|
||||
self.data = {}
|
||||
self.size = Core.LOG_SIZE
|
||||
self.p = 0
|
||||
end,
|
||||
|
||||
-- ship: object
|
||||
-- msg: text
|
||||
-- we immediately remember the name and model, because after some time the object may become inaccessible
|
||||
add = function(self, ship, msg)
|
||||
self.data[self.p%self.size] = { time = Game.time, ship = ship, label = ship and ship:GetLabel(), model = ship and ship:GetShipType(), msg = msg }
|
||||
self.p = self.p + 1
|
||||
end,
|
||||
|
||||
-- an iterator acting like ipairs
|
||||
-- query - the function that receives row, if the function returns false, the row is skipped
|
||||
iter = function(query)
|
||||
return function(self)
|
||||
local stop = self.p > self.size and self.p % self.size or 0
|
||||
if query then
|
||||
return function(a, i)
|
||||
while i ~= stop do
|
||||
i = (i - 1 + a.size) % a.size
|
||||
if query(a.data[i]) then return i, a.data[i] end
|
||||
end
|
||||
end, self, self.p
|
||||
else
|
||||
return function(a, i)
|
||||
if i == stop then return nil
|
||||
else
|
||||
i = (i - 1 + a.size) % a.size
|
||||
return i, a.data[i]
|
||||
end
|
||||
end, self, self.p
|
||||
end
|
||||
end
|
||||
end
|
||||
}
|
||||
Core.log:clear()
|
||||
|
||||
-- register of ships added as Core.ships[ship].attacker
|
||||
do
|
||||
local cache = {}
|
||||
Core.attackers = {
|
||||
add = function(trader, attacker)
|
||||
-- we additionally wrap it in a table in order to refer to a link in
|
||||
-- the trader, and not to the object of the attacking ship itself
|
||||
trader.attacker = { ref = attacker }
|
||||
if not cache[attacker] then
|
||||
-- make weak values so that they are automatically deleted
|
||||
cache[attacker] = setmetatable({ trader.attacker }, { __mode = 'v' })
|
||||
else
|
||||
-- collect links from all traders who were attacked by this ship
|
||||
table.insert(cache[attacker], trader.attacker)
|
||||
end
|
||||
end,
|
||||
-- return true if a link to this ship is in the cache, or clean this
|
||||
-- key if there are no links to it left
|
||||
check = function(attacker)
|
||||
if cache[attacker] then
|
||||
if #cache[attacker] > 0 then return true
|
||||
else cache[attacker] = nil --if the table is empty - delete it
|
||||
end
|
||||
end
|
||||
return false
|
||||
end,
|
||||
-- remove all keys that no longer have links
|
||||
clean = function()
|
||||
for k,v in pairs(cache) do
|
||||
if #v == 0 then cache[k] = nil end
|
||||
end
|
||||
end,
|
||||
getCache = function() return cache end -- for debug
|
||||
}
|
||||
end
|
||||
|
||||
local serialize = function()
|
||||
-- all we need to save is trade_ships, the rest can be rebuilt on load
|
||||
-- The serializer will crash if we try to serialize dead objects (issue #3123)
|
||||
-- also, trade_ships may be nil, because it is cleared in 'onGameEnd', and this may
|
||||
-- happen before the autosave module creates its '_exit' save
|
||||
if Core.ships ~= nil then
|
||||
local count = 0
|
||||
for k,_ in pairs(Core.ships) do
|
||||
if type(k) == 'userdata' and not k:exists() then
|
||||
count = count + 1
|
||||
-- according to the Lua manual, removing items during iteration with pairs() or next() is ok
|
||||
Core.ships[k] = nil
|
||||
end
|
||||
end
|
||||
print('TradeShips: Removed ' .. count .. ' ships before serialization')
|
||||
end
|
||||
return Core.ships
|
||||
end
|
||||
|
||||
local unserialize = function(data)
|
||||
Core.ships = data
|
||||
-- made for backward compatibility with old saves
|
||||
Core.ships.interval = nil
|
||||
end
|
||||
|
||||
Serializer:Register("TradeShips", serialize, unserialize)
|
||||
|
||||
return Core
|
|
@ -0,0 +1,335 @@
|
|||
-- Copyright © 2008-2021 Pioneer Developers. See AUTHORS.txt for details
|
||||
-- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt
|
||||
|
||||
local ShipDef = require 'ShipDef'
|
||||
local debugView = require 'pigui.views.debug'
|
||||
local Engine = require 'Engine'
|
||||
local e = require 'Equipment'
|
||||
local Game = require 'Game'
|
||||
local ui = require 'pigui.baseui'
|
||||
local utils = require 'utils'
|
||||
local Vector2 = _G.Vector2
|
||||
local Space = require 'Space'
|
||||
|
||||
local Core = require 'modules.TradeShips.Core'
|
||||
|
||||
local arrayTable = require 'pigui.libs.array-table'
|
||||
|
||||
-- this module creates a tab in the debug window
|
||||
|
||||
-- UTILS
|
||||
|
||||
-- create a class method caller
|
||||
local function method(fnc, ...)
|
||||
local args = {...}
|
||||
return function(x)
|
||||
return x[fnc](x, table.unpack(args))
|
||||
end
|
||||
end
|
||||
|
||||
-- create a formatting function
|
||||
local function format(fmt)
|
||||
return function(x)
|
||||
return string.format(fmt, x)
|
||||
end
|
||||
end
|
||||
|
||||
-- convert lenght in meters to string in AU
|
||||
local function distanceInAU(meters)
|
||||
return string.format("%.2f AU", meters / Core.AU)
|
||||
end
|
||||
|
||||
local search_text = ""
|
||||
local infosize = 0
|
||||
local statuses = {
|
||||
'inbound',
|
||||
'docked',
|
||||
'hyperspace',
|
||||
'hyperspace_out',
|
||||
'orbit',
|
||||
'fleeing',
|
||||
'outbound',
|
||||
'cowering',
|
||||
'unknown'
|
||||
}
|
||||
|
||||
debugView.registerTab('debug-trade-ships', function()
|
||||
if not Core.ships and not Core.params then return end
|
||||
if not ui.beginTabItem("Tradeships") then return end
|
||||
|
||||
local function property(key, value)
|
||||
ui.withStyleColors({["Text"] = ui.theme.colors.blueFrame}, function()
|
||||
ui.text(key)
|
||||
end)
|
||||
ui.sameLine()
|
||||
ui.text(value)
|
||||
end
|
||||
|
||||
ui.child("tradeships_as_child", Vector2(-1, -infosize), {"NoSavedSettings", "HorizontalScrollbar"}, function()
|
||||
if Core.params then
|
||||
if ui.collapsingHeader("System summary", {"DefaultOpen"}) then
|
||||
local precalculated = {}
|
||||
if Core.params.spawn_in then
|
||||
for _, param in ipairs(Core.params.spawn_in) do
|
||||
precalculated[param[1]] = param[2]
|
||||
end
|
||||
end
|
||||
local number_of = {}
|
||||
for _, status in ipairs(statuses) do
|
||||
number_of[status] = 0
|
||||
end
|
||||
local total_ships = 0
|
||||
for _, trader in pairs(Core.ships) do
|
||||
if number_of[trader.status] then
|
||||
number_of[trader.status] = number_of[trader.status] + 1
|
||||
else
|
||||
number_of['unknown'] = number_of['unknown'] + 1
|
||||
end
|
||||
total_ships = total_ships + 1
|
||||
end
|
||||
|
||||
arrayTable.draw("tradeships_statuses", statuses, arrayTable.addKeys(ipairs, {
|
||||
ships_fact = function(_,v) return number_of[v] end,
|
||||
ships_proj = function(_,v) return precalculated[v] end,
|
||||
ratio = function(_,v) return precalculated[v] and number_of[v] / precalculated[v] * 100 end
|
||||
}),{
|
||||
{ name = "Status", key = 1, string = true },
|
||||
{ name = "Current", key = "ships_fact", },
|
||||
{ name = "Calculated", key = "ships_proj", fnc = format("%.2f") },
|
||||
{ name = "%", key = "ratio", fnc = format("%.2f%%") }
|
||||
},{
|
||||
totals = {{status = "Total", ships_fact = total_ships}}
|
||||
})
|
||||
ui.separator()
|
||||
property("Total flow", string.format("%.2f ship/hour", Core.params.total_flow))
|
||||
ui.sameLine()
|
||||
property("Last spawn interval", ui.Format.Duration(Core.last_spawn_interval))
|
||||
ui.sameLine()
|
||||
property("Lawlessness", string.format("%.4f", Game.system.lawlessness))
|
||||
ui.sameLine()
|
||||
property("Total bodies in space", #Space.GetBodies())
|
||||
end
|
||||
|
||||
if ui.collapsingHeader("Stations") then
|
||||
local totals = {docks = 0, busy_s = 0, landed = 0, flow = 0}
|
||||
-- count the inbound ships
|
||||
local inbound = {}
|
||||
for _, trader in pairs(Core.ships) do
|
||||
local s = trader.status == 'inbound' and trader.starport
|
||||
if s then
|
||||
if inbound[s] then inbound[s] = inbound[s] + 1
|
||||
else inbound[s] = 1
|
||||
end
|
||||
end
|
||||
end
|
||||
totals.label = "Total for " .. utils.count(Core.params.port_params) .. " ports"
|
||||
local obj = Game.systemView:GetSelectedObject()
|
||||
local sb_selected = obj.type == Engine.GetEnumValue("ProjectableTypes", "OBJECT") and obj.base == Engine.GetEnumValue("ProjectableBases", "SYSTEMBODY")
|
||||
arrayTable.draw("tradeships_stationinfo2", Core.params.port_params, arrayTable.addKeys(pairs, {
|
||||
port = function(k,_) return k end,
|
||||
label = function(k,_) return k:GetLabel() end,
|
||||
parent = function(k,_) return k:GetSystemBody().parent.name end,
|
||||
dist = function(k,_) return k:DistanceTo(k:GetSystemBody().nearestJumpable.body) end,
|
||||
docks = function(k,_) totals.docks = totals.docks + k.numDocks return k.numDocks end,
|
||||
busy_s = function(k,_) totals.busy_s = totals.busy_s + k.numShipsDocked return k.numShipsDocked end,
|
||||
inbound = function(k,_) return inbound[k] end,
|
||||
landed = function(_,v) totals.landed = totals.landed + v.landed return v.landed end,
|
||||
flow = function(_,v) totals.flow = totals.flow + v.flow return v.flow end
|
||||
}),{
|
||||
{ name = "Port", key = "label", string = true },
|
||||
{ name = "Parent", key = "parent", string = true },
|
||||
{ name = "Distance", key = "dist", fnc = distanceInAU },
|
||||
{ name = "Docks", key = "docks" },
|
||||
{ name = "Busy", key = "busy", fnc = format("%.2f") },
|
||||
{ name = "Landed", key = "busy_s" },
|
||||
{ name = "Calculated", key = "landed", fnc = format("%.2f") },
|
||||
{ name = "Dock time", key = "time", fnc = format("%.2fh") },
|
||||
{ name = "Inbound", key = "inbound" },
|
||||
{ name = "Ship flow", key = "flow", fnc = format("%.2f ship/h") },
|
||||
},{
|
||||
totals = { totals },
|
||||
callbacks = {
|
||||
onClick = function(row)
|
||||
Game.systemView:SetSelectedObject(Engine.GetEnumValue("ProjectableTypes", "OBJECT"),
|
||||
Engine.GetEnumValue("ProjectableBases", "SYSTEMBODY"), row.port:GetSystemBody())
|
||||
end,
|
||||
isSelected = function(row)
|
||||
return sb_selected and Game.systemView:GetSelectedObject().ref == row.port:GetSystemBody()
|
||||
end
|
||||
}})
|
||||
end
|
||||
|
||||
if ui.collapsingHeader("Local routes") then
|
||||
for shipname, params in pairs(Core.params.local_routes) do
|
||||
ui.text(" ")
|
||||
ui.sameLine()
|
||||
if ui.treeNode(shipname .. " (" .. #params .. ")") then
|
||||
arrayTable.draw("tradeships_" .. shipname .. "_info", params, ipairs, {
|
||||
{ name = "From", key = "from", fnc = method("GetLabel"), string = true },
|
||||
{ name = "To", key = "to", fnc = method("GetLabel"), string = true },
|
||||
{ name = "Duration", key = "duration", fnc = ui.Format.Duration },
|
||||
{ name = "Distance", key = "distance", fnc = distanceInAU }
|
||||
})
|
||||
ui.treePop()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if ui.collapsingHeader("Hyperspace routes") then
|
||||
local function sysName(path)
|
||||
return path:GetStarSystem().name
|
||||
end
|
||||
local selected_in_sectorview = Game.sectorView:GetSelectedSystemPath()
|
||||
for shipname, params in pairs(Core.params.hyper_routes) do
|
||||
if ui.treeNode(shipname .. " (" .. #params .. ")") then
|
||||
arrayTable.draw("tradeships_" .. shipname .. "_hyperinfo", params, ipairs, {
|
||||
{ name = "From", key = "from", fnc = sysName, string = true },
|
||||
{ name = "Distance", key = "distance", fnc = format("%.2f l.y.") },
|
||||
{ name = "Fuel", key = "fuel", fnc = format("%.2f t") },
|
||||
{ name = "Duration", key = "duration", fnc = ui.Format.Duration },
|
||||
{ name = "Cloud Duration", key = "cloud_duration", fnc = ui.Format.Duration }
|
||||
},{
|
||||
callbacks = {
|
||||
onClick = function(row)
|
||||
Game.sectorView:SwitchToPath(row.from)
|
||||
end,
|
||||
isSelected = function(row)
|
||||
return row.from:IsSameSystem(selected_in_sectorview)
|
||||
end
|
||||
}})
|
||||
ui.treePop()
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if ui.collapsingHeader("All ships") then
|
||||
local ships = {}
|
||||
for ship, trader in pairs(Core.ships) do
|
||||
if ship:exists() then
|
||||
table.insert(ships, {
|
||||
ship = ship,
|
||||
label = ship:GetLabel(),
|
||||
status = trader.status,
|
||||
ts_error = trader.ts_error,
|
||||
cargo = ship.usedCargo,
|
||||
ai = ship:GetCurrentAICommand(),
|
||||
model = ship.shipId,
|
||||
port = trader.starport and trader.starport:GetLabel()
|
||||
})
|
||||
end
|
||||
end
|
||||
local obj = Game.systemView:GetSelectedObject()
|
||||
local ship_selected = obj.type == Engine.GetEnumValue("ProjectableTypes", "OBJECT") and obj.base == Engine.GetEnumValue("ProjectableBases", "SHIP")
|
||||
arrayTable.draw("tradeships_all", ships, ipairs, {
|
||||
{ name = "#", key = "#" },
|
||||
{ name = "Label", key = "label", string = true },
|
||||
{ name = "Model", key = "model", string = true },
|
||||
{ name = "Status", key = "status", string = true },
|
||||
{ name = "Error", key = "ts_error", string = true },
|
||||
{ name = "Cargo", key = "cargo" },
|
||||
{ name = "AI", key = "ai", string = true },
|
||||
{ name = "Port", key = "port", string = true }
|
||||
},
|
||||
{ callbacks = {
|
||||
onClick = function(row)
|
||||
if row.status ~= "hyperspace" and row.status ~= "hyperspace_out" then
|
||||
Game.systemView:SetSelectedObject(Engine.GetEnumValue("ProjectableTypes", "OBJECT"),
|
||||
Engine.GetEnumValue("ProjectableBases", "SHIP"), row.ship)
|
||||
end
|
||||
end,
|
||||
isSelected = function(row)
|
||||
return ship_selected and Game.systemView:GetSelectedObject().ref == row.ship
|
||||
end
|
||||
}
|
||||
})
|
||||
end
|
||||
if ui.collapsingHeader("Log") then
|
||||
local obj = Game.systemView:GetSelectedObject()
|
||||
local ship_selected = obj.type == Engine.GetEnumValue("ProjectableTypes", "OBJECT") and obj.base == Engine.GetEnumValue("ProjectableBases", "SHIP")
|
||||
search_text, _ = ui.inputText("Search log", search_text, {})
|
||||
arrayTable.draw("tradeships_log", Core.log, Core.log.iter(search_text ~= "" and function (row)
|
||||
return
|
||||
row.label and string.match(row.label, search_text) or
|
||||
row.msg and string.match(row.msg, search_text) or
|
||||
row.model and string.match(row.model, search_text)
|
||||
end),{
|
||||
{ name = "Time", key = "time" , fnc = ui.Format.Datetime },
|
||||
{ name = "Label", key = "label", string = true },
|
||||
{ name = "Model", key = "model", string = true },
|
||||
{ name = "Message", key = "msg", string = true }
|
||||
},{
|
||||
callbacks = {
|
||||
onClick = function(row)
|
||||
status = Core.ships[row.ship] and Core.ships[row.ship].status
|
||||
if status and status ~= "hyperspace" and status ~= "hyperspace_out" then
|
||||
Game.systemView:SetSelectedObject(Engine.GetEnumValue("ProjectableTypes", "OBJECT"),
|
||||
Engine.GetEnumValue("ProjectableBases", "SHIP"), row.ship)
|
||||
end
|
||||
end,
|
||||
isSelected = function(row)
|
||||
return ship_selected and obj.ref == row.ship
|
||||
end
|
||||
}})
|
||||
end
|
||||
end)
|
||||
|
||||
|
||||
infosize = ui.getCursorScreenPos().y
|
||||
local obj = Game.systemView:GetSelectedObject()
|
||||
if obj.type ~= Engine.GetEnumValue("ProjectableTypes", "NONE") and Core.ships[obj.ref] then
|
||||
local ship = obj.ref
|
||||
local trader = Core.ships[ship]
|
||||
if ui.collapsingHeader("Info:", {"DefaultOpen"}) then
|
||||
property("Trader: ", ship:GetShipType() .. " " .. ship:GetLabel())
|
||||
local status = trader.status
|
||||
if status == "docked" then
|
||||
status = status .. " (" .. trader.starport:GetLabel() .. ")"
|
||||
elseif status == "inbound" then
|
||||
local d,u = ui.Format.Distance(ship:DistanceTo(trader.starport))
|
||||
status = status .. " (" .. trader.starport:GetLabel() .. " - " .. d .. " " .. u .. ")"
|
||||
end
|
||||
property("Status: ", status)
|
||||
if trader.fnc then
|
||||
property("Task: ", trader.fnc .. " in " .. ui.Format.Duration(trader.delay - Game.time))
|
||||
end
|
||||
property("Fuel: ", string.format("%.4f", ship.fuelMassLeft) .. "/" .. ShipDef[ship.shipId].fuelTankMass .. " t")
|
||||
end
|
||||
if ui.collapsingHeader("Internals:", {"DefaultOpen"}) then
|
||||
local equipItems = {}
|
||||
local total_mass = 0
|
||||
local equips = { "cargo", "misc", "hyperspace", "laser" }
|
||||
for _,t in ipairs(equips) do
|
||||
for _,et in pairs(e[t]) do
|
||||
local count = ship:CountEquip(et)
|
||||
if count > 0 then
|
||||
local all_mass = count * et.capabilities.mass
|
||||
table.insert(equipItems, {
|
||||
name = et:GetName(),
|
||||
eq_type = t,
|
||||
count = count,
|
||||
mass = et.capabilities.mass,
|
||||
all_mass = all_mass
|
||||
})
|
||||
total_mass = total_mass + all_mass
|
||||
end
|
||||
end
|
||||
end
|
||||
local capacity = ShipDef[ship.shipId].capacity
|
||||
arrayTable.draw("tradeships_traderequipment", equipItems, ipairs, {
|
||||
{ name = "Name", key = "name", string = true },
|
||||
{ name = "Type", key = "eq_type", string = true },
|
||||
{ name = "Units", key = "count" },
|
||||
{ name = "Unit's mass", key = "mass", fnc = format("%dt") },
|
||||
{ name = "Total", key = "all_mass", fnc = format("%dt") }
|
||||
},
|
||||
{ totals = {
|
||||
{ name = "Total:", all_mass = total_mass },
|
||||
{ name = "Capacity:", all_mass = capacity },
|
||||
{ name = "Free:", all_mass = capacity - total_mass },
|
||||
}})
|
||||
end
|
||||
end
|
||||
infosize = ui.getCursorScreenPos().y - infosize
|
||||
ui.endTabItem()
|
||||
end)
|
|
@ -0,0 +1,326 @@
|
|||
-- Copyright © 2008-2021 Pioneer Developers. See AUTHORS.txt for details
|
||||
-- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt
|
||||
|
||||
local Comms = require 'Comms'
|
||||
local Engine = require 'Engine'
|
||||
local Event = require 'Event'
|
||||
local Game = require 'Game'
|
||||
local ShipDef = require 'ShipDef'
|
||||
local utils = require 'utils'
|
||||
|
||||
local Core = require 'modules.TradeShips.Core'
|
||||
local Flow = require 'modules.TradeShips.Flow'
|
||||
local Trader = require 'modules.TradeShips.Trader'
|
||||
|
||||
-- this module contains all events affecting tradeships
|
||||
|
||||
local onGameStart = function ()
|
||||
if Flow.calculateSystemParams() then
|
||||
if Core.ships == nil then -- new game
|
||||
Flow.spawnInitialShips()
|
||||
Flow.run()
|
||||
else -- deserialization
|
||||
-- check if any trade ships were waiting on a timer
|
||||
-- restart
|
||||
for ship, trader in pairs(Core.ships) do
|
||||
if trader.delay and trader.fnc and trader.delay > Game.time then
|
||||
Core.log:add(ship, "Resume " .. trader.fnc)
|
||||
Trader.assignTask(ship, trader.delay, trader.fnc)
|
||||
end
|
||||
end
|
||||
Flow.run()
|
||||
end
|
||||
end
|
||||
end
|
||||
Event.Register("onGameStart", onGameStart)
|
||||
|
||||
local onEnterSystem = function (ship)
|
||||
-- dont crash when entering unexplored systems
|
||||
if Game.system.explored == false then
|
||||
return
|
||||
end
|
||||
|
||||
-- clearing the cache of ships that attacked traders, but are no longer relevant
|
||||
Core.attackers.clean()
|
||||
|
||||
-- if the player is following a ship through hyperspace that ship may enter first
|
||||
-- so update the system when the first ship enters (see Space::DoHyperspaceTo)
|
||||
if ship:IsPlayer() then
|
||||
Flow.updateTradeShipsTable()
|
||||
if Flow.calculateSystemParams() then
|
||||
Flow.spawnInitialShips()
|
||||
Flow.run()
|
||||
end
|
||||
elseif Core.ships[ship] ~= nil then
|
||||
local trader = Core.ships[ship]
|
||||
Core.log:add(ship, 'Entered '..Game.system.name..' from '..trader.from_path:GetStarSystem().name)
|
||||
|
||||
if trader.route then
|
||||
ship:AIDockWith(trader.route.to)
|
||||
Core.ships[ship]['starport'] = trader.route.to
|
||||
Core.ships[ship]['status'] = 'inbound'
|
||||
else
|
||||
-- starport == nil happens if player has followed ship to empty system, or
|
||||
-- no suitable port found (e.g. all stations atmospheric for ship without atmoshield)
|
||||
Trader.getSystemAndJump(ship)
|
||||
-- if we couldn't reach any systems wait for player to attack
|
||||
end
|
||||
end
|
||||
end
|
||||
Event.Register("onEnterSystem", onEnterSystem)
|
||||
|
||||
local onLeaveSystem = function (ship)
|
||||
if ship:IsPlayer() then
|
||||
-- the next onEnterSystem will be in a new system
|
||||
local total, removed = 0, 0
|
||||
for t_ship, trader in pairs(Core.ships) do
|
||||
total = total + 1
|
||||
if trader.status == 'hyperspace' then
|
||||
if trader.dest_path:IsSameSystem(Game.system.path) then
|
||||
-- remove ships that are in hyperspace to here
|
||||
Core.ships[t_ship] = nil
|
||||
removed = removed + 1
|
||||
end
|
||||
else
|
||||
-- remove all ships that are not in hyperspace
|
||||
Core.ships[t_ship] = nil
|
||||
removed = removed + 1
|
||||
end
|
||||
end
|
||||
Core.log:add(nil, 'onLeaveSystem:total:'..total..',removed:'..removed)
|
||||
elseif Core.ships[ship] ~= nil then
|
||||
local system = Core.ships[ship]['dest_path']:GetStarSystem()
|
||||
Core.log:add(ship, 'Left '..Game.system.name..' for '.. system.name)
|
||||
Flow.cleanTradeShipsTable()
|
||||
end
|
||||
end
|
||||
Event.Register("onLeaveSystem", onLeaveSystem)
|
||||
|
||||
local onFrameChanged = function (ship)
|
||||
if not ship:isa("Ship") or Core.ships[ship] == nil then return end
|
||||
local trader = Core.ships[ship]
|
||||
Core.log:add(ship, "Entered frame " .. (ship.frameBody and ship.frameBody:GetLabel() or "unknown"))
|
||||
|
||||
if trader.status == 'outbound' then
|
||||
-- the cloud inherits the ship velocity and vector
|
||||
ship:CancelAI()
|
||||
if Trader.getSystemAndJump(ship) ~= 'OK' then
|
||||
ship:AIDockWith(trader.starport)
|
||||
trader['status'] = 'inbound'
|
||||
trader.ts_error = 'cnt_jump_frame'
|
||||
end
|
||||
end
|
||||
end
|
||||
Event.Register("onFrameChanged", onFrameChanged)
|
||||
|
||||
local onShipDocked = function (ship, starport)
|
||||
if Core.ships[ship] == nil then return end
|
||||
local trader = Core.ships[ship]
|
||||
|
||||
trader.route = nil
|
||||
Core.log:add(ship, 'Docked with '..starport.label)
|
||||
|
||||
if trader.status == 'fleeing' then
|
||||
trader['status'] = 'cowering'
|
||||
else
|
||||
trader['status'] = 'docked'
|
||||
end
|
||||
if trader.chance then
|
||||
trader['chance'] = trader.chance / 2
|
||||
trader['last_flee'], trader['no_jump'] = nil, nil
|
||||
end
|
||||
|
||||
-- 'sell' trade cargo
|
||||
for cargo, _ in pairs(ship:GetCargo()) do
|
||||
ship:RemoveEquip(cargo, 1000000)
|
||||
end
|
||||
|
||||
local damage = ShipDef[trader.ship_name].hullMass - ship.hullMassLeft
|
||||
if damage > 0 then
|
||||
ship:SetHullPercent()
|
||||
Trader.addEquip(ship)
|
||||
end
|
||||
Trader.addFuel(ship)
|
||||
ship:SetFuelPercent()
|
||||
|
||||
if trader.status == 'docked' then
|
||||
Trader.assignTask(ship, Game.time + utils.deviation(Core.params.port_params[starport].time * 3600, 0.8), 'doUndock')
|
||||
end
|
||||
end
|
||||
Event.Register("onShipDocked", onShipDocked)
|
||||
|
||||
local onShipUndocked = function (ship, starport)
|
||||
if Core.ships[ship] == nil then return end
|
||||
|
||||
-- fly to the limit of the starport frame
|
||||
ship:AIFlyTo(starport)
|
||||
|
||||
Core.ships[ship]['status'] = 'outbound'
|
||||
end
|
||||
Event.Register("onShipUndocked", onShipUndocked)
|
||||
|
||||
local onAICompleted = function (ship, ai_error)
|
||||
if Core.ships[ship] == nil then return end
|
||||
local trader = Core.ships[ship]
|
||||
if ai_error ~= 'NONE' then
|
||||
Core.log:add(ship, 'AICompleted: Error: '..ai_error..' Status: '..trader.status) end
|
||||
|
||||
if trader.status == 'outbound' then
|
||||
if Trader.getSystemAndJump(ship) ~= 'OK' then
|
||||
ship:AIDockWith(trader.starport)
|
||||
trader['status'] = 'inbound'
|
||||
trader.ts_error = 'cnt_jump_aicomp'
|
||||
end
|
||||
elseif trader.status == 'orbit' then
|
||||
if ai_error == 'NONE' then
|
||||
trader.ts_error = "wait_6h"
|
||||
Trader.assignTask(ship, Game.time + 21600, 'doRedock')
|
||||
end
|
||||
-- XXX if ORBIT_IMPOSSIBLE asteroid? get parent of parent and attempt orbit?
|
||||
elseif trader.status == 'inbound' then
|
||||
if ai_error == 'REFUSED_PERM' then
|
||||
Trader.doOrbit(ship)
|
||||
trader.ts_error = "refused_perm"
|
||||
end
|
||||
end
|
||||
end
|
||||
Event.Register("onAICompleted", onAICompleted)
|
||||
|
||||
local onShipLanded = function (ship, _)
|
||||
if Core.ships[ship] == nil then return end
|
||||
Core.log:add(ship, 'Landed: '..Core.ships[ship].starport.label)
|
||||
Trader.doOrbit(ship)
|
||||
end
|
||||
Event.Register("onShipLanded", onShipLanded)
|
||||
|
||||
local onShipAlertChanged = function (ship, alert)
|
||||
if Core.ships[ship] == nil then return end
|
||||
if alert == 'SHIP_FIRING' then
|
||||
Core.log:add(ship, 'Alert changed to '..alert) end
|
||||
local trader = Core.ships[ship]
|
||||
if trader.attacker == nil then return end
|
||||
|
||||
if alert == 'NONE' or not trader.attacker.ref:exists() or
|
||||
(alert == 'SHIP_NEARBY' and ship:DistanceTo(trader.attacker.ref) > 100) then
|
||||
if trader.status == 'fleeing' then
|
||||
-- had not reached starport yet
|
||||
trader['status'] = 'inbound'
|
||||
trader.ts_error = "alert!"
|
||||
elseif trader.status == 'cowering' then
|
||||
-- already reached starport and docked
|
||||
-- we need to do everything that the ship does after docking
|
||||
onShipDocked(ship, trader.starport)
|
||||
end
|
||||
end
|
||||
end
|
||||
Event.Register("onShipAlertChanged", onShipAlertChanged)
|
||||
|
||||
local onShipHit = function (ship, attacker)
|
||||
if attacker == nil then return end-- XX
|
||||
|
||||
-- XXX this whole thing might be better if based on amount of damage sustained
|
||||
if Core.ships[ship] == nil then return end
|
||||
local trader = Core.ships[ship]
|
||||
trader.ts_error = "HIT"
|
||||
|
||||
trader['chance'] = trader.chance or 0
|
||||
trader['chance'] = trader.chance + 0.1
|
||||
|
||||
-- don't spam actions
|
||||
if trader.last_flee and Game.time - trader.last_flee < Engine.rand:Integer(5, 7) then return end
|
||||
|
||||
-- if outbound jump now
|
||||
if trader.status == 'outbound' then
|
||||
if Trader.getSystemAndJump(ship) == 'OK' then
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
trader['status'] = 'fleeing'
|
||||
Core.attackers.add(trader, attacker)
|
||||
|
||||
-- update last_flee
|
||||
trader['last_flee'] = Game.time
|
||||
|
||||
-- if distance to starport is far attempt to hyperspace
|
||||
if trader.no_jump ~= true then
|
||||
local starports = Core.params.local_routes
|
||||
if #starports == 0 then
|
||||
trader['no_jump'] = true -- it already tried in onEnterSystem
|
||||
elseif trader.starport and Engine.rand:Number(1) < trader.chance then
|
||||
local distance = ship:DistanceTo(trader.starport)
|
||||
if distance > Core.AU * (2 - trader.chance) then
|
||||
if Trader.getSystemAndJump(ship) then
|
||||
return
|
||||
else
|
||||
trader['no_jump'] = true
|
||||
trader['chance'] = trader.chance + 0.3
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- maybe jettison a bit of cargo
|
||||
if Engine.rand:Number(1) < trader.chance then
|
||||
local cargo_type = nil
|
||||
local max_cap = ShipDef[ship.shipId].capacity
|
||||
for k, v in pairs(ship:GetCargo()) do
|
||||
if v > 1 and Engine.rand:Number(1) < v / max_cap then
|
||||
cargo_type = k
|
||||
break
|
||||
end
|
||||
end
|
||||
if cargo_type and ship:Jettison(cargo_type) then
|
||||
Comms.ImportantMessage(attacker.label..', take this and leave us be, you filthy pirate!', ship.label)
|
||||
trader['chance'] = trader.chance - 0.1
|
||||
end
|
||||
end
|
||||
end
|
||||
Event.Register("onShipHit", onShipHit)
|
||||
|
||||
local onShipCollided = function (ship, other)
|
||||
if Core.ships[ship] == nil then return end
|
||||
if other:isa('CargoBody') then return end
|
||||
Core.ships[ship].ts_error = "collided"
|
||||
|
||||
if other:isa('Ship') and other:IsPlayer() then
|
||||
onShipHit(ship, other)
|
||||
return
|
||||
end
|
||||
end
|
||||
Event.Register("onShipCollided", onShipCollided)
|
||||
|
||||
local onShipDestroyed = function (ship, attacker)
|
||||
if attacker == nil then return end-- XX
|
||||
if Core.ships[ship] ~= nil then
|
||||
local trader = Core.ships[ship]
|
||||
Core.log:add(ship, 'Destroyed by '..attacker.label..', status:'..trader.status..' starport:'..(trader.starport and trader.starport.label or 'N/A'))
|
||||
Core.ships[ship] = nil
|
||||
-- XXX consider spawning some CargoBodies if killed by a ship
|
||||
elseif Core.attackers.check(attacker) then
|
||||
for t_ship, trader in pairs(Core.ships) do
|
||||
if trader.attacker and trader.attacker.ref == ship then
|
||||
trader.attacker = nil
|
||||
if trader.status == 'fleeing' then
|
||||
-- had not reached starport yet
|
||||
trader['status'] = 'inbound'
|
||||
trader.ts_error = "attack!"
|
||||
elseif trader.status == 'cowering' then
|
||||
-- already reached starport and docked
|
||||
-- we need to do everything that the ship does after docking
|
||||
onShipDocked(t_ship, trader.starport)
|
||||
end
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Event.Register("onShipDestroyed", onShipDestroyed)
|
||||
|
||||
|
||||
local onGameEnd = function ()
|
||||
-- drop the references for our data so Lua can free them
|
||||
-- and so we can start fresh if the player starts another game
|
||||
Core.ships, Core.params = nil, nil
|
||||
end
|
||||
Event.Register("onGameEnd", onGameEnd)
|
|
@ -0,0 +1,525 @@
|
|||
-- Copyright © 2008-2021 Pioneer Developers. See AUTHORS.txt for details
|
||||
-- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt
|
||||
|
||||
local e = require 'Equipment'
|
||||
local Engine = require 'Engine'
|
||||
local Game = require 'Game'
|
||||
local Ship = require 'Ship'
|
||||
local ShipDef = require 'ShipDef'
|
||||
local Space = require 'Space'
|
||||
local utils = require 'utils'
|
||||
|
||||
local Core = require 'modules.TradeShips.Core'
|
||||
local Trader = require 'modules.TradeShips.Trader'
|
||||
|
||||
local Flow = {}
|
||||
-- this module creates all tradeships, and ensures the flow of new ships into the system
|
||||
|
||||
-- UTILS
|
||||
|
||||
-- the inbound cloud lifetime cannot be more than half the route duration, and
|
||||
-- asymptotically approaches two days
|
||||
local function inboundCloudDuration(route_duration)
|
||||
local max_duration = 2 * 24 * 60 * 60 -- 2 days max
|
||||
return utils.asymptote(route_duration / 2, max_duration, 0.0)
|
||||
end
|
||||
|
||||
-- change of the standard parameter depending on local factors
|
||||
local function localFactors(param, system)
|
||||
-- we only take into account the criminal situation
|
||||
return param * (1 - system.lawlessness)
|
||||
end
|
||||
|
||||
-- return an array of names of ships that (at first sight) can be traders
|
||||
local getAcceptableShips = function ()
|
||||
-- accept all ships with the hyperdrive, in fact
|
||||
local filter_function = function(_,def)
|
||||
-- XXX should limit to ships large enough to carry significant
|
||||
-- cargo, but we don't have enough ships yet
|
||||
return def.tag == 'SHIP' and def.hyperdriveClass > 0 -- and def.roles.merchant
|
||||
end
|
||||
return utils.build_array(
|
||||
utils.map(function (k,def)
|
||||
return k,def.id
|
||||
end,
|
||||
utils.filter(filter_function,
|
||||
pairs(ShipDef)
|
||||
)))
|
||||
end
|
||||
|
||||
-- expand the search radius until we find a sufficient number of nearby systems
|
||||
local function getNearSystems()
|
||||
local from_systems, dist = {}, 10
|
||||
while #from_systems < Core.MINIMUM_NEAR_SYSTEMS do
|
||||
from_systems = Game.system:GetNearbySystems(dist)
|
||||
dist = dist + 5
|
||||
end
|
||||
local paths = {}
|
||||
for _, from_system in ipairs(from_systems) do
|
||||
table.insert(paths, from_system.path)
|
||||
end
|
||||
return paths
|
||||
end
|
||||
|
||||
-- get a measure of the market size and build lists of imports and exports
|
||||
local function getImportsExports(system)
|
||||
local import_score, export_score = 0, 0
|
||||
local imports, exports = {}, {}
|
||||
for key, equip in pairs(e.cargo) do
|
||||
local v = system:GetCommodityBasePriceAlterations(key)
|
||||
if key ~= "rubbish" and key ~= "radioactives" and system:IsCommodityLegal(key) then
|
||||
-- values from SystemInfoView::UpdateEconomyTab
|
||||
|
||||
if v > 2 then
|
||||
import_score = import_score + (v > 10 and 2 or 1)
|
||||
table.insert(imports, equip)
|
||||
end
|
||||
|
||||
if v < -4 then
|
||||
export_score = export_score + (v < -10 and 2 or 1)
|
||||
table.insert(exports, equip)
|
||||
end
|
||||
end
|
||||
end
|
||||
return imports, exports
|
||||
end
|
||||
|
||||
-- we collect all the information necessary for the module to work in this system:
|
||||
-- valid ship models
|
||||
-- import / export goods
|
||||
-- hyperspace routes (from other stars to the star(s) of this system)
|
||||
-- local routes (from the star(s) to the trade ports)
|
||||
-- trade ports parameters
|
||||
-- flow rate required to maintain the required number of trade ships
|
||||
Flow.calculateSystemParams = function()
|
||||
-- delete the old value if it was, all calculation results will be written here
|
||||
Core.params = nil
|
||||
|
||||
local system = Game.system
|
||||
|
||||
-- dont spawn tradeships in unpopulated systems
|
||||
if system.population == 0 then return nil end
|
||||
|
||||
-- all ports in the system
|
||||
local ports = Space.GetBodies(function (body) return body.superType == 'STARPORT' end)
|
||||
-- check if the current system can be traded in
|
||||
if #ports == 0 then return nil end
|
||||
|
||||
-- all routes in the system
|
||||
-- routes are laid from the nearest star
|
||||
local routes = {}
|
||||
for _,port in ipairs(ports) do
|
||||
local from = port:GetSystemBody().nearestJumpable.body
|
||||
local distance = from:DistanceTo(port)
|
||||
table.insert(routes, {
|
||||
from = from,
|
||||
to = port,
|
||||
distance = distance,
|
||||
weight = port.numDocks / distance * Core.AU
|
||||
})
|
||||
end
|
||||
|
||||
-- get ships listed as tradeships, if none - give up
|
||||
local ship_names = getAcceptableShips()
|
||||
if #ship_names == 0 then return nil end
|
||||
|
||||
local imports, exports = getImportsExports(system)
|
||||
|
||||
-- if there is no market then there is no trade
|
||||
if #imports == 0 or #exports == 0 then return nil end
|
||||
|
||||
-- all tradeship models array
|
||||
-- these are real ship objects, they are needed for calculations
|
||||
-- but at the same time they will not be inserted into space, so as not to generate events, etc.
|
||||
-- after this function completes, they should be eaten by the garbage collector
|
||||
local dummies = {}
|
||||
for _, ship_name in ipairs(ship_names) do
|
||||
local dummy = Ship.Create(ship_name)
|
||||
Trader.addEquip(dummy)
|
||||
-- just fill it with hydrogen to the brim
|
||||
dummy:AddEquip(e.cargo.hydrogen, Trader.emptySpace(dummy))
|
||||
dummies[ship_name] = dummy
|
||||
end
|
||||
|
||||
-- build all hyperspace inbound routes for all ships
|
||||
local near_systems = getNearSystems()
|
||||
local hyper_routes = {}
|
||||
for i = #ship_names, 1, -1 do
|
||||
local id = ship_names[i]
|
||||
local this_ship_routes = {}
|
||||
for _, path in ipairs(near_systems) do
|
||||
local status, distance, fuel, duration = dummies[id]:GetHyperspaceDetails(path, system.path)
|
||||
if status == "OK" then
|
||||
table.insert(this_ship_routes, {from = path, distance = distance, fuel = fuel, duration = duration})
|
||||
end
|
||||
end
|
||||
if #this_ship_routes > 0 then
|
||||
hyper_routes[id] = this_ship_routes
|
||||
else
|
||||
print("WARNING: ship " .. id .. " discarded, because there are no hyperspace routes available for it")
|
||||
table.remove(ship_names, i)
|
||||
end
|
||||
end
|
||||
|
||||
-- build all suitable local routes for all ships
|
||||
local local_routes = {}
|
||||
for i = #ship_names, 1, -1 do
|
||||
local id = ship_names[i]
|
||||
local this_ship_routes = {}
|
||||
for _,route in ipairs(routes) do
|
||||
-- discard route if ship can't dock/land there
|
||||
if Trader.isStarportAcceptableForShip(route.to, dummies[id]) then
|
||||
local duration = dummies[id]:GetDurationForDistance(route.distance)
|
||||
-- this value determines the popularity of this station among merchants
|
||||
-- the closer it is, and the more docks in it, the more popular it is
|
||||
local weight = math.pow(route.to.numDocks, 2) / math.pow(duration, 2)
|
||||
table.insert(this_ship_routes, {
|
||||
from = route.from,
|
||||
to = route.to,
|
||||
distance = route.distance,
|
||||
ndocks = route.to.numDocks,
|
||||
duration = duration,
|
||||
weight = weight,
|
||||
})
|
||||
end
|
||||
end
|
||||
if #this_ship_routes == 0 then
|
||||
-- no suitable local routes for this ship
|
||||
print("WARNING: ship " .. id .. " discarded, because there are no local routes available for it")
|
||||
table.remove(ship_names, i)
|
||||
-- also erasing this ship from hyperspace routes
|
||||
hyper_routes[id] = nil
|
||||
else
|
||||
utils.normWeights(this_ship_routes)
|
||||
local_routes[id] = this_ship_routes
|
||||
end
|
||||
end
|
||||
|
||||
-- as a result, not a single ship might remain
|
||||
if #ship_names == 0 then return nil end
|
||||
|
||||
-- now we have only those ships that can jump from somewhere and land somewhere
|
||||
-- now set the flow on the most popular route to MAX_ROUTE_FLOW
|
||||
-- let's collect all data on routes and calculate the most popular route weight
|
||||
local local_routes_map = {}
|
||||
for _,routes_for_ship in pairs(local_routes) do
|
||||
for _, route_for_ship in ipairs(routes_for_ship) do
|
||||
local current = local_routes_map[route_for_ship.to] or 0
|
||||
local_routes_map[route_for_ship.to] = current + route_for_ship.weight
|
||||
end
|
||||
end
|
||||
local max_route_weight = 0
|
||||
for _, weight in pairs(local_routes_map) do
|
||||
max_route_weight = math.max(max_route_weight, weight)
|
||||
end
|
||||
|
||||
-- weight to flow conversion factor
|
||||
local flow_per_ship = localFactors(Core.MAX_ROUTE_FLOW, system) / max_route_weight
|
||||
-- we will reduce it, depending on the legal status of the system
|
||||
|
||||
local total_flow
|
||||
-- calculate flow for local routes
|
||||
-- and total count of ships in space
|
||||
-- it may be necessary to recalculate the flow if the required number of
|
||||
-- ships turns out to be too large
|
||||
do
|
||||
local ships_in_space
|
||||
local function calculateFlowForLocalRoutes()
|
||||
ships_in_space = 0
|
||||
total_flow = 0
|
||||
for _, ship_routes in pairs(local_routes) do
|
||||
for _, ship_route in ipairs(ship_routes) do
|
||||
ship_route.flow = flow_per_ship * ship_route.weight
|
||||
total_flow = total_flow + ship_route.flow
|
||||
ship_route.amount = ship_route.flow * ship_route.duration / 3600
|
||||
ships_in_space = ships_in_space + ship_route.amount
|
||||
end
|
||||
end
|
||||
end
|
||||
calculateFlowForLocalRoutes()
|
||||
-- checking if we shouldn't reduce the number of ships
|
||||
local allowed_ships = utils.asymptote(ships_in_space, Core.MAX_SHIPS, 0.6)
|
||||
if ships_in_space > allowed_ships then
|
||||
print(string.format("WARNING: lowering ships flow by %.2f for performance reasons", allowed_ships / ships_in_space))
|
||||
-- the required amount is too large
|
||||
-- lowering the flow for performance purposes
|
||||
flow_per_ship = flow_per_ship * allowed_ships / ships_in_space
|
||||
-- recalculate for new conditions
|
||||
calculateFlowForLocalRoutes()
|
||||
end
|
||||
end
|
||||
|
||||
-- calculate station's parameters:
|
||||
-- summarize the flow to the station from all suitable ships
|
||||
local port_params = {}
|
||||
for _, ship_routes in pairs(local_routes) do
|
||||
for _, ship_route in ipairs(ship_routes) do
|
||||
local to = ship_route.to
|
||||
if not port_params[to] then port_params[to] = {flow = 0} end
|
||||
port_params[to].flow = port_params[to].flow + ship_route.flow
|
||||
end
|
||||
end
|
||||
|
||||
local old_total_flow = total_flow
|
||||
-- now we need to prevent too much flow to the station
|
||||
for port, params in pairs(port_params) do
|
||||
local max_flow = port.numDocks * localFactors(Core.MAX_BUSY, system) / Core.MIN_STATION_DOCKING_TIME
|
||||
if params.flow > max_flow then
|
||||
local k = max_flow / params.flow
|
||||
params.flow = max_flow
|
||||
for _, ship_routes in pairs(local_routes) do
|
||||
for _, ship_route in ipairs(ship_routes) do
|
||||
if ship_route.to == port then
|
||||
local old_flow = ship_route.flow
|
||||
ship_route.flow = ship_route.flow * k
|
||||
ship_route.weight = ship_route.weight * k
|
||||
total_flow = total_flow - old_flow + ship_route.flow
|
||||
end
|
||||
end
|
||||
end
|
||||
print(port:GetLabel() .. ": the flow has been reduced so that the stay at the station is not too short")
|
||||
end
|
||||
end
|
||||
|
||||
if old_total_flow ~= total_flow then
|
||||
print("old total flow: " .. old_total_flow .. " new total flow: " .. total_flow)
|
||||
for _, ship_routes in pairs(local_routes) do
|
||||
utils.normWeights(ship_routes)
|
||||
end
|
||||
end
|
||||
|
||||
-- now we know the total flow, we can calculate the flow and the number of ships in hyperspace
|
||||
-- adding flow information to hyper_routes
|
||||
for _, id in ipairs(ship_names) do
|
||||
-- the probabilities are the same for all hyper-routes of a given ship
|
||||
local p = 1 / #ship_names / #hyper_routes[id]
|
||||
for _, row in ipairs(hyper_routes[id]) do
|
||||
row.cloud_duration = inboundCloudDuration(row.duration)
|
||||
row.flow = p * total_flow
|
||||
row.ships = row.flow * row.cloud_duration / 3600
|
||||
end
|
||||
end
|
||||
|
||||
-- calculate station parking times
|
||||
-- calculate the maximum flow to the station
|
||||
local max_station_flow = 0
|
||||
for _, row in pairs(port_params) do
|
||||
if row.flow > max_station_flow then
|
||||
max_station_flow = row.flow
|
||||
end
|
||||
end
|
||||
|
||||
-- conversion factor flow -> station_load
|
||||
-- this is quite artifical calculation
|
||||
-- we want the maximum station load (occupied pads/all pads) to be <MAX_BUSY>
|
||||
-- and we also want ships to stay longer at less popular stations
|
||||
-- to have some kind of presence
|
||||
-- therefore, we make the dependence of the load on the flow not linear but radical
|
||||
local flow_to_busy = localFactors(Core.MAX_BUSY, system) / math.sqrt(max_station_flow) -- conversion factor
|
||||
-- we also reduce the workload of stations, depending on the criminal situation
|
||||
for port, params in pairs(port_params) do
|
||||
params.ndocks = port.numDocks
|
||||
-- parking time cannot be less than the specified minimum
|
||||
params.landed = math.max(math.sqrt(params.flow) * flow_to_busy * params.ndocks, Core.MIN_STATION_DOCKING_TIME * params.flow)
|
||||
params.busy = params.landed / params.ndocks
|
||||
params.time = params.landed / params.flow
|
||||
end
|
||||
|
||||
Core.params = {
|
||||
-- list of ship names that can actually trade in this system
|
||||
ship_names = ship_names,
|
||||
-- market parameters
|
||||
imports = imports,
|
||||
exports = exports,
|
||||
-- ship flow
|
||||
total_flow = total_flow,
|
||||
-- available routes lookup table by ship name
|
||||
local_routes = local_routes,
|
||||
-- available hyperspace routes lookup table by ship name
|
||||
hyper_routes = hyper_routes,
|
||||
-- station parameters lookup table by it's path
|
||||
port_params = port_params
|
||||
}
|
||||
return true
|
||||
end
|
||||
|
||||
-- run this function at the start of the game or when jumped to another system
|
||||
Flow.spawnInitialShips = function()
|
||||
|
||||
if not Core.params then return nil end
|
||||
|
||||
local hyper_routes = Core.params.hyper_routes
|
||||
local local_routes = Core.params.local_routes
|
||||
local port_params = Core.params.port_params
|
||||
|
||||
-- we want to calculate how many ships of each model are in space, hyperspace
|
||||
-- and stations on average
|
||||
-- it is needed for initial spawn
|
||||
|
||||
-- create tables for all possible ship placement options
|
||||
-- table for ships flying in space
|
||||
local routes_variants = {}
|
||||
-- table for ships docked at station
|
||||
local docked_variants = {}
|
||||
-- amount of ships in space at start
|
||||
local ships_in_space = 0
|
||||
for id, ship_routes in pairs(local_routes) do
|
||||
for _, ship_route in ipairs(ship_routes) do
|
||||
table.insert(
|
||||
routes_variants, {
|
||||
id = id,
|
||||
from = ship_route.from,
|
||||
to = ship_route.to,
|
||||
weight = ship_route.amount
|
||||
})
|
||||
table.insert(
|
||||
docked_variants, {
|
||||
id = id,
|
||||
port = ship_route.to,
|
||||
weight = ship_route.flow
|
||||
})
|
||||
ships_in_space = ships_in_space + ship_route.amount
|
||||
end
|
||||
end
|
||||
local port_summary = {}
|
||||
for k, v in pairs(port_params) do
|
||||
v.port = k
|
||||
table.insert(port_summary, v)
|
||||
end
|
||||
utils.normWeights(routes_variants)
|
||||
utils.normWeights(docked_variants)
|
||||
|
||||
-- amount of ships docked at start
|
||||
local ships_docked = 0
|
||||
for _, params in pairs(port_params) do
|
||||
ships_docked = ships_docked + params.landed
|
||||
end
|
||||
|
||||
-- table for ships in inbound hyperspace cloud
|
||||
local cloud_variants = {}
|
||||
-- amount of ships in cloud at start
|
||||
local ships_in_cloud = 0
|
||||
for id, ship_routes in pairs(hyper_routes) do
|
||||
for _, ship_route in ipairs(ship_routes) do
|
||||
ships_in_cloud = ships_in_cloud + ship_route.ships
|
||||
table.insert(cloud_variants, {
|
||||
ship = id,
|
||||
from = ship_route.from,
|
||||
cloud_duration = ship_route.cloud_duration,
|
||||
weight = ship_route.ships
|
||||
})
|
||||
end
|
||||
end
|
||||
utils.normWeights(cloud_variants)
|
||||
|
||||
-- if we generate ships after jumping into the system, and not when starting a new game,
|
||||
-- count the number of ships that were transferred to this system from the
|
||||
-- source system, and reduce the need for generation by this amount
|
||||
if Core.ships == nil then Core.ships = {} end
|
||||
for ship, trader in pairs(Core.ships) do
|
||||
if trader.status == 'hyperspace' then
|
||||
ships_in_cloud = math.max(0, ships_in_cloud - 1)
|
||||
-- and also assign a random port for this ship, if there are any ports for it at all
|
||||
local routes_for_ship = Core.params.local_routes[ship.shipId]
|
||||
if routes_for_ship then
|
||||
trader.route = utils.chooseNormalized(routes_for_ship)
|
||||
else
|
||||
-- TODO but what if there are no ports for it?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- selection table where the ship will be at the start
|
||||
local spawn_in = {
|
||||
{place = "inbound", weight = ships_in_space},
|
||||
{place = "hyperspace", weight = ships_in_cloud},
|
||||
{place = "docked", weight = ships_docked}
|
||||
}
|
||||
-- we remember this data for output to the debug table
|
||||
Core.params.spawn_in = {{"inbound", ships_in_space}, {"hyperspace", ships_in_cloud}, {"docked", ships_docked}}
|
||||
utils.normWeights(spawn_in)
|
||||
|
||||
-- generating ships
|
||||
for _ = 1, ships_in_space + ships_in_cloud + ships_docked do
|
||||
--choose place
|
||||
local place = utils.chooseNormalized(spawn_in).place
|
||||
|
||||
if place == "inbound" then
|
||||
local params = utils.chooseNormalized(routes_variants)
|
||||
local hj_route = utils.chooseEqual(hyper_routes[params.id])
|
||||
local ship = Space.SpawnShip(params.id, 9, 11, {hj_route.from, params.from:GetSystemBody().path, 0.0})
|
||||
ship:SetLabel(Ship.MakeRandomLabel())
|
||||
Core.ships[ship] = { ts_error = "OK", status = 'inbound', starport = params.to, ship_name = params.id}
|
||||
Trader.addEquip(ship)
|
||||
local fuel_added = Trader.addFuel(ship)
|
||||
Trader.addCargo(ship, 'import')
|
||||
if fuel_added and fuel_added > 0 then
|
||||
ship:RemoveEquip(e.cargo.hydrogen, math.min(hj_route.fuel, fuel_added))
|
||||
end
|
||||
Space.PutShipOnRoute(ship, params.to, Engine.rand:Number(0.0, 0.999))-- don't generate at the destination
|
||||
ship:AIDockWith(params.to)
|
||||
|
||||
elseif place == "hyperspace" then
|
||||
-- choose random source system, and ship
|
||||
local cloud = utils.chooseNormalized(cloud_variants)
|
||||
local route = utils.chooseNormalized(Core.params.local_routes[cloud.ship])
|
||||
Trader.spawnInCloud(cloud.ship, cloud, route, Game.time + Engine.rand:Integer(1, cloud.cloud_duration))
|
||||
|
||||
else -- docked
|
||||
local params = utils.chooseNormalized(docked_variants)
|
||||
local ship = Space.SpawnShipDocked(params.id, params.port)
|
||||
-- if can't spawn - just skip
|
||||
if ship then
|
||||
Core.ships[ship] = { ts_error = "OK", status = 'docked', starport = params.port, ship_name = params.id }
|
||||
ship:SetLabel(Ship.MakeRandomLabel())
|
||||
Trader.addEquip(ship)
|
||||
end
|
||||
end
|
||||
end
|
||||
return ships_in_space
|
||||
end
|
||||
|
||||
Flow.run = function()
|
||||
local ship_name = utils.chooseEqual(Core.params.ship_names)
|
||||
local cloud = utils.chooseEqual(Core.params.hyper_routes[ship_name])
|
||||
-- we immediately choose the local route, because it depends on which star to
|
||||
-- jump to in the multiple system
|
||||
local route = utils.chooseNormalized(Core.params.local_routes[ship_name])
|
||||
Trader.spawnInCloud(ship_name, cloud, route, Game.time + utils.deviation(cloud.cloud_duration, 0.1))
|
||||
Core.last_spawn_interval = utils.deviation(3600 / Core.params.total_flow, 0.9) -- for debug info
|
||||
Trader.callInThisSystem(Game.time + Core.last_spawn_interval, Flow.run)
|
||||
end
|
||||
|
||||
Flow.cleanTradeShipsTable = function()
|
||||
local HYPERCLOUD_DURATION = 172800 -- 2 days
|
||||
local total, hyperspace, removed = 0, 0, 0
|
||||
for ship, trader in pairs(Core.ships) do
|
||||
total = total + 1
|
||||
if trader.status == 'hyperspace_out' then
|
||||
hyperspace = hyperspace + 1
|
||||
-- remove well past due ships as the player can not catch them
|
||||
if trader.jump_time + HYPERCLOUD_DURATION < Game.time then
|
||||
Core.ships[ship] = nil
|
||||
removed = removed + 1
|
||||
end
|
||||
end
|
||||
end
|
||||
-- print('cleanTSTable:total:'..total..',active:'..total - hyperspace..',removed:'..removed)
|
||||
end
|
||||
|
||||
-- we leave only those ships that flew into this system
|
||||
Flow.updateTradeShipsTable = function()
|
||||
local total, removed = 0, 0
|
||||
for ship, trader in pairs(Core.ships) do
|
||||
total = total + 1
|
||||
if trader.status ~= 'hyperspace_out' or not trader.dest_path:IsSameSystem(Game.system.path) then
|
||||
Core.ships[ship] = nil
|
||||
removed = removed + 1
|
||||
else
|
||||
trader.status = 'hyperspace'
|
||||
end
|
||||
end
|
||||
-- print('updateTSTable:total:'..total..',removed:'..removed)
|
||||
end
|
||||
|
||||
return Flow
|
|
@ -0,0 +1,338 @@
|
|||
-- Copyright © 2008-2021 Pioneer Developers. See AUTHORS.txt for details
|
||||
-- Licensed under the terms of the GPL v3. See licenses/GPL-3.txt
|
||||
|
||||
local e = require 'Equipment'
|
||||
local Engine = require 'Engine'
|
||||
local Game = require 'Game'
|
||||
local Ship = require 'Ship'
|
||||
local ShipDef = require 'ShipDef'
|
||||
local Space = require 'Space'
|
||||
local Timer = require 'Timer'
|
||||
|
||||
local Core = require 'modules.TradeShips.Core'
|
||||
|
||||
local Trader = {}
|
||||
-- this module contains functions that work for single traders
|
||||
|
||||
Trader.addEquip = function (ship)
|
||||
assert(ship.usedCargo == 0, "equipment is only installed on an empty ship")
|
||||
local ship_type = ShipDef[ship.shipId]
|
||||
|
||||
-- add standard equipment
|
||||
ship:AddEquip(e.hyperspace['hyperdrive_'..tostring(ship_type.hyperdriveClass)])
|
||||
if ShipDef[ship.shipId].equipSlotCapacity.atmo_shield > 0 then
|
||||
ship:AddEquip(e.misc.atmospheric_shielding)
|
||||
end
|
||||
ship:AddEquip(e.misc.radar)
|
||||
ship:AddEquip(e.misc.autopilot)
|
||||
ship:AddEquip(e.misc.cargo_life_support)
|
||||
|
||||
-- add defensive equipment based on lawlessness, luck and size
|
||||
local lawlessness = Game.system.lawlessness
|
||||
local size_factor = ship.freeCapacity ^ 2 / 2000000
|
||||
|
||||
if Engine.rand:Number(1) - 0.1 < lawlessness then
|
||||
local num = math.floor(math.sqrt(ship.freeCapacity / 50)) -
|
||||
ship:CountEquip(e.misc.shield_generator)
|
||||
if num > 0 then ship:AddEquip(e.misc.shield_generator, num) end
|
||||
if ship_type.equipSlotCapacity.energy_booster > 0 and
|
||||
Engine.rand:Number(1) + 0.5 - size_factor < lawlessness then
|
||||
ship:AddEquip(e.misc.shield_energy_booster)
|
||||
end
|
||||
end
|
||||
|
||||
-- we can't use these yet
|
||||
if ship_type.equipSlotCapacity.ecm > 0 then
|
||||
if Engine.rand:Number(1) + 0.2 < lawlessness then
|
||||
ship:AddEquip(e.misc.ecm_advanced)
|
||||
elseif Engine.rand:Number(1) < lawlessness then
|
||||
ship:AddEquip(e.misc.ecm_basic)
|
||||
end
|
||||
end
|
||||
|
||||
-- this should be rare
|
||||
if ship_type.equipSlotCapacity.hull_autorepair > 0 and
|
||||
Engine.rand:Number(1) + 0.75 - size_factor < lawlessness then
|
||||
ship:AddEquip(e.misc.hull_autorepair)
|
||||
end
|
||||
end
|
||||
|
||||
Trader.addCargo = function (ship, direction)
|
||||
local total = 0
|
||||
local empty_space = math.min(ship.freeCapacity, ship:GetEquipFree("cargo"))
|
||||
local size_factor = empty_space / 20
|
||||
local ship_cargo = {}
|
||||
|
||||
local cargoTypes = direction == 'import' and Core.params.imports or Core.params.exports
|
||||
|
||||
if #cargoTypes == 1 then
|
||||
total = ship:AddEquip(cargoTypes[1], empty_space)
|
||||
ship_cargo[cargoTypes[1]] = total
|
||||
elseif #cargoTypes > 1 then
|
||||
|
||||
-- happens if there was very little space left to begin with (eg small
|
||||
-- ship with lots of equipment). if we let it through then we end up
|
||||
-- trying to add 0 cargo forever
|
||||
if size_factor < 1 then
|
||||
if Core.ships[ship] then
|
||||
Core.ships[ship]['cargo'] = ship_cargo
|
||||
end
|
||||
return 0
|
||||
end
|
||||
|
||||
while total < empty_space do
|
||||
-- get random for direction
|
||||
local cargo_type = cargoTypes[Engine.rand:Integer(1, #cargoTypes)]
|
||||
|
||||
-- amount based on price and size of ship
|
||||
local num = math.abs(Game.system:GetCommodityBasePriceAlterations(cargo_type.name)) * size_factor
|
||||
num = Engine.rand:Integer(num, num * 2)
|
||||
|
||||
local added = ship:AddEquip(cargo_type, num)
|
||||
if ship_cargo[cargo_type] == nil then
|
||||
ship_cargo[cargo_type] = added
|
||||
else
|
||||
ship_cargo[cargo_type] = ship_cargo[cargo_type] + added
|
||||
end
|
||||
total = total + added
|
||||
end
|
||||
end
|
||||
if Core.ships[ship] then
|
||||
Core.ships[ship]['cargo'] = ship_cargo
|
||||
end
|
||||
-- if the table for direction was empty then cargo is empty and total is 0
|
||||
return total
|
||||
end
|
||||
|
||||
Trader.doOrbit = function (ship)
|
||||
local trader = Core.ships[ship]
|
||||
local sbody = trader.starport.path:GetSystemBody()
|
||||
local body = Space.GetBody(sbody.parent.index)
|
||||
ship:AIEnterLowOrbit(body)
|
||||
trader['status'] = 'orbit'
|
||||
Core.log:add(ship, 'Ordering orbit of '..body.label)
|
||||
end
|
||||
|
||||
local getSystem = function (ship)
|
||||
local max_range = ship:GetEquip('engine', 1):GetMaximumRange(ship)
|
||||
max_range = math.min(max_range, 30)
|
||||
local min_range = max_range / 2;
|
||||
local systems_in_range = Game.system:GetNearbySystems(min_range)
|
||||
if #systems_in_range == 0 then
|
||||
systems_in_range = Game.system:GetNearbySystems(max_range)
|
||||
end
|
||||
if #systems_in_range == 0 then
|
||||
return nil end
|
||||
if #systems_in_range == 1 then
|
||||
return systems_in_range[1].path
|
||||
end
|
||||
|
||||
local target_system = nil
|
||||
local best_prices = 0
|
||||
|
||||
-- find best system for cargo
|
||||
for _, next_system in ipairs(systems_in_range) do
|
||||
if #next_system:GetStationPaths() > 0 then
|
||||
local next_prices = 0
|
||||
for cargo, count in pairs(ship:GetCargo()) do
|
||||
next_prices = next_prices + (next_system:GetCommodityBasePriceAlterations(cargo.name) * count)
|
||||
end
|
||||
if next_prices > best_prices then
|
||||
target_system, best_prices = next_system, next_prices
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if target_system == nil then
|
||||
-- pick a random system as fallback
|
||||
target_system = systems_in_range[Engine.rand:Integer(1, #systems_in_range)]
|
||||
|
||||
-- get closer systems
|
||||
local systems_half_range = Game.system:GetNearbySystems(min_range)
|
||||
|
||||
if #systems_half_range > 1 then
|
||||
target_system = systems_half_range[Engine.rand:Integer(1, #systems_half_range)]
|
||||
end
|
||||
end
|
||||
|
||||
-- pick a random starport, if there are any, so the game can simulate
|
||||
-- travel to it if player arrives after (see Space::DoHyperspaceTo)
|
||||
local target_starport_paths = target_system:GetStationPaths()
|
||||
if #target_starport_paths > 0 then
|
||||
return target_starport_paths[Engine.rand:Integer(1, #target_starport_paths)]
|
||||
end
|
||||
|
||||
return target_system.path
|
||||
end
|
||||
|
||||
local jumpToSystem = function (ship, target_path)
|
||||
if target_path == nil then return nil end
|
||||
|
||||
local status, _, duration = ship:HyperjumpTo(target_path)
|
||||
|
||||
if status ~= 'OK' then
|
||||
Core.log:add(ship, 'jump status is not OK: ' .. status)
|
||||
return status
|
||||
end
|
||||
|
||||
-- update table for ship
|
||||
Core.ships[ship]['status'] = 'hyperspace_out'
|
||||
Core.ships[ship]['starport'] = nil
|
||||
Core.ships[ship]['dest_time'] = Game.time + duration
|
||||
Core.ships[ship]['jump_time'] = Game.time
|
||||
Core.ships[ship]['dest_path'] = target_path
|
||||
Core.ships[ship]['from_path'] = Game.system.path
|
||||
return status
|
||||
end
|
||||
|
||||
Trader.getSystemAndJump = function (ship)
|
||||
-- attention all coders: trade_ships[ship].starport may be nil
|
||||
if Core.ships[ship].starport then
|
||||
local body = Space.GetBody(Core.ships[ship].starport.path:GetSystemBody().parent.index)
|
||||
local port = Core.ships[ship].starport
|
||||
-- boost away from the starport before jumping if it is too close
|
||||
if (ship:DistanceTo(port) < 20000) then
|
||||
ship:AIEnterLowOrbit(body)
|
||||
end
|
||||
return jumpToSystem(ship, getSystem(ship))
|
||||
end
|
||||
end
|
||||
|
||||
Trader.emptySpace = function(ship)
|
||||
return math.min(ship.freeCapacity, ship:GetEquipFree("cargo"))
|
||||
end
|
||||
|
||||
local function isAtmo(starport)
|
||||
return starport.type ~= 'STARPORT_ORBITAL' and starport.path:GetSystemBody().parent.hasAtmosphere
|
||||
end
|
||||
|
||||
local function canAtmo(ship)
|
||||
return ship:CountEquip(e.misc.atmospheric_shielding) ~= 0
|
||||
end
|
||||
|
||||
Trader.isStarportAcceptableForShip = function(starport, ship)
|
||||
return canAtmo(ship) or not isAtmo(starport)
|
||||
-- TODO add a check to see if the ship has enough engine power to handle gravity
|
||||
end
|
||||
|
||||
Trader.getNearestStarport = function(ship, current)
|
||||
-- get all available routes for this model of ship
|
||||
local routes = Core.params.local_routes[ship.shipId]
|
||||
if not routes or #routes == 0 then return nil end
|
||||
|
||||
-- Find the nearest starport that we can land at (other than current)
|
||||
local starport, distance
|
||||
for i = 1, #routes do
|
||||
local next_starport = routes[i].to
|
||||
if next_starport ~= current then
|
||||
local next_distance = ship:DistanceTo(next_starport)
|
||||
if Trader.isStarportAcceptableForShip(next_starport, ship) and ((starport == nil) or (next_distance < distance)) then
|
||||
starport, distance = next_starport, next_distance
|
||||
end
|
||||
end
|
||||
end
|
||||
return starport or current
|
||||
end
|
||||
|
||||
Trader.addFuel = function (ship)
|
||||
local drive = ship:GetEquip('engine', 1)
|
||||
|
||||
-- a drive must be installed
|
||||
if not drive then
|
||||
Core.log:add(ship, 'Ship has no drive!')
|
||||
return nil
|
||||
end
|
||||
|
||||
-- the last character of the fitted drive is the class
|
||||
-- the fuel needed for max range is the square of the drive class
|
||||
local count = drive.capabilities.hyperclass ^ 2
|
||||
|
||||
-- account for fuel it already has
|
||||
count = count - ship:CountEquip(e.cargo.hydrogen)
|
||||
|
||||
local added = ship:AddEquip(e.cargo.hydrogen, count)
|
||||
|
||||
return added
|
||||
end
|
||||
|
||||
-- TRADER DEFERRED TASKS
|
||||
--
|
||||
-- call the passed function in a specified time, checking whether we are still
|
||||
-- in this system
|
||||
Trader.callInThisSystem = function(t, fnc)
|
||||
local current_path = Game.system.path
|
||||
Timer:CallAt(t, function()
|
||||
if Game.system and current_path:IsSameSystem(Game.system.path) then fnc() end
|
||||
end)
|
||||
end
|
||||
|
||||
local trader_task = {}
|
||||
-- a table of functions that can be assigned for delayed execution by the trader
|
||||
-- { ["fnc1"] = fnc1, ... }
|
||||
-- made to serialize pending job execution
|
||||
-- the task function prototype should be:
|
||||
-- function(ship)
|
||||
|
||||
trader_task.doUndock = function(ship)
|
||||
-- the player may have left the system or the ship may have already undocked
|
||||
if ship:exists() and ship:GetDockedWith() then
|
||||
-- we load goods before departure
|
||||
Trader.addCargo(ship, 'export')
|
||||
if not ship:Undock() then
|
||||
-- unable to undock, try again later
|
||||
Trader.assignTask(ship, Game.time + Core.WAIT_FOR_NEXT_UNDOCK, 'doUndock')
|
||||
return true
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
trader_task.doRedock = function(ship)
|
||||
trader = Core.ships[ship]
|
||||
if trader then
|
||||
if ship:exists() and ship.flightState ~= 'HYPERSPACE' then
|
||||
trader['starport'] = Trader.getNearestStarport(ship, trader.starport)
|
||||
ship:AIDockWith(trader.starport)
|
||||
trader['status'] = 'inbound'
|
||||
trader.ts_error = "dock_aft_6h"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- at the appointed time the specified function will be executed, in addition,
|
||||
-- it will be executed only in the same system in which it is assigned, and
|
||||
-- also the assignment can be serialized
|
||||
-- ship: object
|
||||
-- delay: absolute game time, when to execute the task
|
||||
-- fn_name: string, one of the keys in the table 'trader_task'
|
||||
Trader.assignTask = function(ship, delay, fn_name)
|
||||
local trader = Core.ships[ship]
|
||||
if trader then
|
||||
trader.delay = delay
|
||||
trader.fnc = fn_name
|
||||
Trader.callInThisSystem(delay, function()
|
||||
trader.delay = nil
|
||||
trader.fnc = nil
|
||||
trader_task[fn_name](ship)
|
||||
end)
|
||||
end
|
||||
end
|
||||
|
||||
Trader.spawnInCloud = function(ship_name, cloud, route, dest_time)
|
||||
-- choose random local route for choosen ship,
|
||||
-- the hyperjump target depends on this (for multiple system)
|
||||
ship = Space.SpawnShip(ship_name, 0, 0, {cloud.from, route.from:GetSystemBody().path, dest_time})
|
||||
ship:SetLabel(Ship.MakeRandomLabel())
|
||||
Trader.addEquip(ship)
|
||||
Trader.addFuel(ship)
|
||||
Trader.addCargo(ship, 'import')
|
||||
Core.ships[ship] = {
|
||||
status = 'hyperspace',
|
||||
dest_time = dest_time,
|
||||
dest_path = Game.system.path,
|
||||
from_path = cloud.from,
|
||||
ship_name = ship_name,
|
||||
route = route
|
||||
}
|
||||
end
|
||||
|
||||
return Trader
|
|
@ -0,0 +1,226 @@
|
|||
local ui = require 'pigui.baseui'
|
||||
array_table = {}
|
||||
|
||||
--
|
||||
-- Table: array_table
|
||||
--
|
||||
|
||||
--
|
||||
-- Function: array_table.draw
|
||||
--
|
||||
-- Draw a table based on the contents of the "tbl" array
|
||||
--
|
||||
-- > array_table.draw(id, tbl, iter, column, extra)
|
||||
--
|
||||
-- Parameters:
|
||||
--
|
||||
-- id - unique string identifier
|
||||
--
|
||||
-- tbl - an array of hashtables with the same keys - main data source
|
||||
--
|
||||
-- iter - pairs, ipairs, e.t.c.
|
||||
--
|
||||
-- columns - array of hashtables, determining the presence and order of columns. keys:
|
||||
-- name - column name
|
||||
-- key - key name from table
|
||||
-- fnc - function to convert value to string
|
||||
-- string - if true, the given column is sorted as a string, false - as number
|
||||
--
|
||||
-- extra
|
||||
-- totals - table similar to tbl, its contents are shown at the very botton, under the separator
|
||||
-- callbacks
|
||||
-- onClick - this function is called if the line is clicked
|
||||
-- isSelected - this function is called for every row and if it returns true the row is highlighed
|
||||
--
|
||||
-- Example:
|
||||
--
|
||||
-- > table_array.draw("hyperspace_params", params, ipairs,
|
||||
-- > {
|
||||
-- > { name = "From", key = "from", fnc = sysName, string = true },
|
||||
-- > { name = "Distance", key = "distance", fnc = format("%.2f l.y.") },
|
||||
-- > { name = "Fuel", key = "fuel", fnc = format("%.2f t") },
|
||||
-- > { name = "Duration", key = "duration", fnc = ui.Format.Duration },
|
||||
-- > { name = "Cloud Duration", key = "cloud_duration", fnc = ui.Format.Duration }
|
||||
-- > },{
|
||||
-- > callbacks = {
|
||||
-- > onClick = function(row)
|
||||
-- > Game.sectorView:SwitchToPath(row.from)
|
||||
-- > end,
|
||||
-- > isSelected = function(row)
|
||||
-- > return row.from:IsSameSystem(selected_in_sectorview)
|
||||
-- > end
|
||||
-- > }})
|
||||
|
||||
local sort_param = {} -- we need to keep sorting options between frames
|
||||
|
||||
array_table.draw = function(id, tbl, iter, columns, extra)
|
||||
local callbacks = extra and extra.callbacks
|
||||
local data = {}
|
||||
-- sort so that nil always goes down the table
|
||||
local function less (a, b) return a and ( not b or a < b ) end
|
||||
local function greater(a, b) return a and ( not b or b < a ) end
|
||||
local sort_symbol = {
|
||||
[less] = { sym = "▼ ", nxt = greater },
|
||||
[greater] = { sym = "▲ ", nxt = less }
|
||||
}
|
||||
if not sort_param[id] then sort_param[id] = {} end
|
||||
|
||||
-- build table
|
||||
for i, row0 in iter(tbl) do
|
||||
local row = {}
|
||||
for j,column_cfg in ipairs(columns) do
|
||||
if column_cfg.key == "#" then
|
||||
row[j] = i
|
||||
else
|
||||
row[j] = row0[column_cfg.key]
|
||||
end
|
||||
end
|
||||
-- remember the original row for callbacks
|
||||
row[#columns+1] = row0
|
||||
table.insert(data, row)
|
||||
end
|
||||
local widths = {}
|
||||
for i = 1, #columns do widths[i] = 0 end
|
||||
|
||||
-- draw table
|
||||
-- top line
|
||||
ui.columns(#columns, id, false)
|
||||
for i, column_cfg in ipairs(columns) do
|
||||
if sort_param[id][column_cfg.key] then
|
||||
local key = column_cfg.key
|
||||
local fnc = sort_param[id][key]
|
||||
if column_cfg.string then
|
||||
local string_fnc = column_cfg.fnc or tostring
|
||||
table.sort(data, function(a, b)
|
||||
return fnc(a[i] and string_fnc(a[i]), b[i] and string_fnc(b[i]))
|
||||
end)
|
||||
else
|
||||
table.sort(data, function(a, b) return fnc(a[i], b[i]) end)
|
||||
end
|
||||
if ui.selectable(sort_symbol[fnc].sym .. column_cfg.name .. "##" .. id, false, {}) then
|
||||
sort_param[id] = { [column_cfg.key] = sort_symbol[fnc].nxt }
|
||||
end
|
||||
else
|
||||
if ui.selectable(column_cfg.name .. "##" .. id, false, {}) then
|
||||
sort_param[id] = { [column_cfg.key] = less }
|
||||
end
|
||||
end
|
||||
widths[i] = math.max(widths[i], ui.calcTextSize(sort_symbol[less].sym .. column_cfg.name .. "--").x)
|
||||
ui.nextColumn()
|
||||
end
|
||||
ui.separator()
|
||||
-- other lines
|
||||
local highlight_box
|
||||
local selected_boxes = {}
|
||||
local top_left, down_right = 0, 0
|
||||
-- draw next cell and recalculate column width
|
||||
local function putCell(item, i, fnc)
|
||||
if not fnc then fnc = tostring end
|
||||
local txt = item and fnc(item) or "-"
|
||||
ui.text(txt)
|
||||
widths[i] = math.max(widths[i], ui.calcTextSize(txt .. "--").x)
|
||||
end
|
||||
if #data > 0 then
|
||||
for _,item in ipairs(data) do
|
||||
top_left = ui.getCursorScreenPos()
|
||||
for i,_ in ipairs(columns) do
|
||||
putCell(item[i], i, columns[i].fnc)
|
||||
down_right = ui.getCursorScreenPos()
|
||||
down_right.x = down_right.x + ui.getColumnWidth()
|
||||
ui.nextColumn()
|
||||
end
|
||||
down_right.y = ui.getCursorScreenPos().y - 2
|
||||
top_left.y = top_left.y - 2
|
||||
if callbacks and callbacks.isSelected and callbacks.isSelected(item[#columns + 1]) then
|
||||
table.insert(selected_boxes, { top_left, down_right })
|
||||
end
|
||||
if not highlight_box and ui.isWindowHovered() and ui.isMouseHoveringRect(top_left, down_right, false) then
|
||||
highlight_box = { top_left, down_right }
|
||||
if ui.isMouseClicked(0) and callbacks and callbacks.onClick then
|
||||
callbacks.onClick( item[#columns + 1] ) -- in the additional column we have saved the original row
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
-- totals
|
||||
if extra and extra.totals then
|
||||
ui.separator()
|
||||
for _, row in ipairs(extra.totals) do
|
||||
for i,column_cfg in ipairs(columns) do
|
||||
putCell(row[column_cfg.key], i, column_cfg.fnc)
|
||||
ui.nextColumn()
|
||||
end
|
||||
end
|
||||
end
|
||||
-- set column width
|
||||
for i = 1,#widths do ui.setColumnWidth(i-1, widths[i]) end
|
||||
-- end columns
|
||||
ui.columns(1, "")
|
||||
-- draw highlight
|
||||
if highlight_box then
|
||||
ui.addRectFilled(highlight_box[1], highlight_box[2], ui.theme.colors.white:opacity(0.3), 0, 0)
|
||||
end
|
||||
if #selected_boxes ~= 0 then
|
||||
for _, selected_box in ipairs(selected_boxes) do
|
||||
ui.addRectFilled(selected_box[1], selected_box[2], ui.theme.colors.white:opacity(0.2), 0, 0)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--
|
||||
-- Function: array_table.addkeys
|
||||
--
|
||||
-- create stateless iterator to create "virtual" keys on the fly
|
||||
-- the k,v from iterator is passed to function, and the value that it returns is written to given key
|
||||
--
|
||||
-- > myiter = addKeys(iter, config)
|
||||
--
|
||||
-- Parameters:
|
||||
--
|
||||
-- iter - pairs, ipairs e.t.c.
|
||||
--
|
||||
-- config - hashtable:
|
||||
-- key1 = fnc1(k,v) ... return value1 end
|
||||
-- key2 = fnc2(k,v) ... return value2 end
|
||||
-- ...
|
||||
--
|
||||
-- Return:
|
||||
--
|
||||
--- Example:
|
||||
--
|
||||
-- > local a = { {one = 10, two = 20},
|
||||
-- > {one = 15, two = 18} }
|
||||
-- > local addSum = addKeys(ipairs, { sum = function(k,v) return v.one + v.two end } )
|
||||
-- >
|
||||
-- > for k, v in addSum(a) do
|
||||
-- > print(k, v.one, v.two, v.sum)
|
||||
-- > end
|
||||
-- >
|
||||
-- > 1, 10, 20, 30
|
||||
-- > 2, 15, 18, 33
|
||||
--
|
||||
array_table.addKeys = function(iter, config)
|
||||
return function(tbl)
|
||||
local step, context, position = iter(tbl)
|
||||
return function(a, i)
|
||||
local row0
|
||||
i, row0 = step(a, i)
|
||||
if row0 then
|
||||
local row = {}
|
||||
if type(row0) == 'table' then
|
||||
for key, value in pairs(row0) do
|
||||
row[key] = value
|
||||
end
|
||||
else
|
||||
row[1] = row0
|
||||
end
|
||||
for key, fnc in pairs(config) do
|
||||
row[key] = fnc(i, row0)
|
||||
end
|
||||
return i, row
|
||||
end
|
||||
end, context, position
|
||||
end
|
||||
end
|
||||
|
||||
return array_table
|
|
@ -73,6 +73,8 @@ ui.getProjectedBodiesGrouped = pigui.GetProjectedBodiesGrouped
|
|||
ui.isMouseReleased = pigui.IsMouseReleased
|
||||
ui.isMouseHoveringRect = pigui.IsMouseHoveringRect
|
||||
ui.collapsingHeader = pigui.CollapsingHeader
|
||||
ui.treeNode = pigui.TreeNode
|
||||
ui.treePop = pigui.TreePop
|
||||
ui.beginPopupModal = pigui.BeginPopupModal
|
||||
ui.endPopup = pigui.EndPopup
|
||||
ui.openPopup = pigui.OpenPopup
|
||||
|
|
|
@ -175,7 +175,7 @@ ui.Format = {
|
|||
local s = number < 0.0 and "-" or ""
|
||||
number = math.abs(number)
|
||||
local fmt = "%." .. (places or '2') .. "f%s"
|
||||
if number < 1e3 then return s .. number
|
||||
if number < 1e3 then return s .. fmt:format(number, "")
|
||||
elseif number < 1e6 then return s .. math.floor(number / 1e3) .. "," .. number % 1e3
|
||||
elseif number < 1e9 then return s .. fmt:format(number / 1e6, "mil")
|
||||
elseif number < 1e12 then return s .. fmt:format(number / 1e9, "bil")
|
||||
|
|
|
@ -234,7 +234,7 @@ void Body::UpdateFrame()
|
|||
FrameId parent = frame->GetParent();
|
||||
Frame *newFrame = Frame::GetFrame(parent);
|
||||
if (newFrame) { // don't fall out of root frame
|
||||
Output("%s leaves frame %s\n", GetLabel().c_str(), frame->GetLabel().c_str());
|
||||
Log::Verbose("{} leaves frame{}\n", GetLabel(), frame->GetLabel());
|
||||
SwitchToFrame(parent);
|
||||
return;
|
||||
}
|
||||
|
@ -246,7 +246,7 @@ void Body::UpdateFrame()
|
|||
const vector3d pos = GetPositionRelTo(kid);
|
||||
if (pos.Length() >= kid_frame->GetRadius()) continue;
|
||||
SwitchToFrame(kid);
|
||||
Output("%s enters frame %s\n", GetLabel().c_str(), kid_frame->GetLabel().c_str());
|
||||
Log::Verbose("{} enters frame{}\n", GetLabel(), frame->GetLabel());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -289,14 +289,6 @@ void Game::TimeStep(float step)
|
|||
if (Pi::game->GetTime() >= m_hyperspaceEndTime) {
|
||||
SwitchToNormalSpace();
|
||||
m_player->EnterSystem();
|
||||
// event "onEnterSystem(player)" is in the event queue after p_player->EnterSystem()
|
||||
// in this callback, for example, fuel is reduced
|
||||
// but event handling has already passed, and will only be in the next frame
|
||||
// it turns out that we are already in the system, but the fuel has not yet been reduced
|
||||
// and then the drawing of the views will go, and inconsistencies may occur,
|
||||
// for example, the sphere of the hyperjump range will be too large
|
||||
// so we forcefully call event handling again
|
||||
LuaEvent::Emit();
|
||||
RequestTimeAccel(TIMEACCEL_1X);
|
||||
} else
|
||||
m_hyperspaceProgress += step;
|
||||
|
|
|
@ -32,10 +32,15 @@
|
|||
// StarSystem *s = Pi::GetGalaxy()->GetStarSystem(SystemPath(0,0,0,0));
|
||||
// LuaObject<StarSystem>::PushToLua(s);
|
||||
//
|
||||
// // Heap-allocated, Lua will get a copy
|
||||
// // Stack-allocated, Lua will get a copy
|
||||
// SystemPath path(0,0,0,0,1);
|
||||
// LuaObject<SystemPath>::PushToLua(path);
|
||||
//
|
||||
// // Create an object, fully owned by lua
|
||||
// // WARNING! the lifetime of an object will be determined by the lua garbage
|
||||
// // collector, so a pointer to it should not be stored in any form on the C++ side
|
||||
// LuaObject<Ship>::CreateInLua(ship_id); // constructor arguments are passed
|
||||
//
|
||||
// Get an object from the Lua stack at index n. Causes a Lua exception if the
|
||||
// object doesn't exist or the types don't match.
|
||||
//
|
||||
|
@ -208,6 +213,8 @@ public:
|
|||
static inline void PushToLua(DeleteEmitter *o); // LuaCoreObject
|
||||
static inline void PushToLua(RefCounted *o); // LuaSharedObject
|
||||
static inline void PushToLua(const T &o); // LuaCopyObject
|
||||
template <typename... Args>
|
||||
static inline void CreateInLua(Args &&... args);
|
||||
|
||||
template <typename Ret, typename Key, typename... Args>
|
||||
static inline Ret CallMethod(T *o, const Key &key, const Args &... args);
|
||||
|
@ -354,6 +361,30 @@ private:
|
|||
LuaRef m_ref;
|
||||
};
|
||||
|
||||
// wrapper for a "lua-owned" object.
|
||||
// the wrapper is deleted by lua gc, and when it is deleted, it also deletes the wrapped object
|
||||
template <typename T>
|
||||
class LuaOwnObject : public LuaObject<T> {
|
||||
public:
|
||||
LuaOwnObject(T *o)
|
||||
{
|
||||
m_object = o;
|
||||
}
|
||||
|
||||
~LuaOwnObject()
|
||||
{
|
||||
delete (m_object);
|
||||
}
|
||||
|
||||
LuaWrappable *GetObject() const
|
||||
{
|
||||
return m_object;
|
||||
}
|
||||
|
||||
private:
|
||||
T *m_object;
|
||||
};
|
||||
|
||||
// push methods, create wrappers if necessary
|
||||
// wrappers are allocated from Lua memory
|
||||
template <typename T>
|
||||
|
@ -376,6 +407,14 @@ inline void LuaObject<T>::PushToLua(const T &o)
|
|||
Register(new (LuaObjectBase::Allocate(sizeof(LuaCopyObject<T>))) LuaCopyObject<T>(o));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
template <typename... Args>
|
||||
inline void LuaObject<T>::CreateInLua(Args &&... args)
|
||||
{
|
||||
T *p(new T(std::forward<Args>(args)...));
|
||||
Register(new (LuaObjectBase::Allocate(sizeof(LuaOwnObject<T>))) LuaOwnObject<T>(static_cast<T *>(p)));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
template <typename Ret, typename Key, typename... Args>
|
||||
inline Ret LuaObject<T>::CallMethod(T *o, const Key &key, const Args &... args)
|
||||
|
|
|
@ -1863,7 +1863,7 @@ static int l_pigui_get_projected_bodies_grouped(lua_State *l)
|
|||
for (Body *body : Pi::game->GetSpace()->GetBodies()) {
|
||||
if (body == Pi::game->GetPlayer()) continue;
|
||||
if (body->GetType() == ObjectType::PROJECTILE) continue;
|
||||
if ((body->GetType() == ObjectType::SHIP || body->GetType() == ObjectType::CARGOBODY) &&
|
||||
if ((body->GetType() == ObjectType::SHIP || body->GetType() == ObjectType::CARGOBODY || body->GetType() == ObjectType::HYPERSPACECLOUD) &&
|
||||
body->GetPositionRelTo(Pi::player).Length() > ship_max_distance) continue;
|
||||
const PiGui::TScreenSpace res = lua_world_space_to_screen_space(body); // defined in LuaPiGui.cpp
|
||||
if (!res._onScreen) continue;
|
||||
|
@ -2653,6 +2653,75 @@ static int l_pigui_collapsing_header(lua_State *l)
|
|||
return 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Function: treeNode
|
||||
*
|
||||
* Start drawing a tree node
|
||||
*
|
||||
* > opened = ui.treeNode(label, flags)
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* > if ui.treeNode("things", {"DefaultOpen"}) then
|
||||
* > ...
|
||||
* > if ui.treeNode("subthings") then
|
||||
* > ...
|
||||
* > ui.treePop()
|
||||
* > end
|
||||
* > ...
|
||||
* > ui.treePop()
|
||||
* > end
|
||||
*
|
||||
* Parameters:
|
||||
*
|
||||
* label - string, headline
|
||||
* flag - <TreeNodeFlags>
|
||||
*
|
||||
* Returns:
|
||||
*
|
||||
* opened - bool, treenode opens when you click no the triangle at the beginning of the line
|
||||
*
|
||||
*/
|
||||
static int l_pigui_treenode(lua_State *l)
|
||||
{
|
||||
PROFILE_SCOPED()
|
||||
std::string label = LuaPull<std::string>(l, 1);
|
||||
ImGuiTreeNodeFlags flags = LuaPull<ImGuiTreeNodeFlags_>(l, 2, ImGuiTreeNodeFlags_None);
|
||||
LuaPush(l, ImGui::TreeNodeEx(label.c_str(), flags));
|
||||
return 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Function: treePop
|
||||
*
|
||||
* End drawing a tree node
|
||||
*
|
||||
* > ui.treePop()
|
||||
*
|
||||
* Example:
|
||||
*
|
||||
* > if ui.treeNode("things", {"DefaultOpen"}) then
|
||||
* > ...
|
||||
* > if ui.treeNode("subthings") then
|
||||
* > ...
|
||||
* > ui.treePop()
|
||||
* > end
|
||||
* > ...
|
||||
* > ui.treePop()
|
||||
* > end
|
||||
*
|
||||
* Returns:
|
||||
*
|
||||
* nothing
|
||||
*
|
||||
*/
|
||||
static int l_pigui_treepop(lua_State *l)
|
||||
{
|
||||
PROFILE_SCOPED()
|
||||
ImGui::TreePop();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static int l_pigui_push_text_wrap_pos(lua_State *l)
|
||||
{
|
||||
PROFILE_SCOPED()
|
||||
|
@ -2831,6 +2900,8 @@ void LuaObject<PiGui::Instance>::RegisterClass()
|
|||
{ "Combo", l_pigui_combo },
|
||||
{ "ListBox", l_pigui_listbox },
|
||||
{ "CollapsingHeader", l_pigui_collapsing_header },
|
||||
{ "TreeNode", l_pigui_treenode },
|
||||
{ "TreePop", l_pigui_treepop },
|
||||
{ "CaptureMouseFromApp", l_pigui_capture_mouse_from_app },
|
||||
{ "PlotHistogram", l_pigui_plot_histogram },
|
||||
{ "ProgressBar", l_pigui_progress_bar },
|
||||
|
|
|
@ -16,6 +16,8 @@
|
|||
#include "Space.h"
|
||||
#include "SpaceStation.h"
|
||||
#include "ship/PlayerShipController.h"
|
||||
#include "ship/PrecalcPath.h"
|
||||
#include "src/lua.h"
|
||||
|
||||
/*
|
||||
* Class: Ship
|
||||
|
@ -660,6 +662,42 @@ static int l_ship_is_ecm_ready(lua_State *l)
|
|||
return 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Method: GetDurationForDistance
|
||||
*
|
||||
* Calculating the duration of the flight of a given ship at a specified distance.
|
||||
* Assumed that at the beginning and at the end of the travel the speed is 0.
|
||||
*
|
||||
* > duration = ship:GetDurationForDistance(distance)
|
||||
*
|
||||
* Parameters:
|
||||
*
|
||||
* distance - length, in meters
|
||||
*
|
||||
* Result:
|
||||
*
|
||||
* duration - travel time, in seconds.
|
||||
*
|
||||
*/
|
||||
static int l_ship_get_duration_for_distance(lua_State *l)
|
||||
{
|
||||
Ship *ship = LuaObject<Ship>::CheckFromLua(1);
|
||||
double distance = LuaPull<double>(l, 2);
|
||||
const ShipType *st = ship->GetShipType();
|
||||
const shipstats_t ss = ship->GetStats();
|
||||
PrecalcPath pp(
|
||||
distance, // distance
|
||||
0.0, // velocity at start
|
||||
st->effectiveExhaustVelocity,
|
||||
st->linThrust[THRUSTER_FORWARD],
|
||||
st->linAccelerationCap[THRUSTER_FORWARD],
|
||||
1000 * (ss.static_mass + ss.fuel_tank_mass_left), // 100% mass of the ship
|
||||
1000 * ss.fuel_tank_mass_left * 0.8, // multipied to 0.8 have fuel reserve
|
||||
0.85); // braking margin
|
||||
LuaPush<double>(l, pp.getFullTime());
|
||||
return 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Method: InitiateHyperjumpTo
|
||||
*
|
||||
|
@ -751,6 +789,30 @@ static int l_ship_abort_hyperjump(lua_State *l)
|
|||
return 0;
|
||||
}
|
||||
|
||||
/*
|
||||
* Method: Create
|
||||
*
|
||||
* Create a new ship object by type id (string)
|
||||
* The ship is not added to space.
|
||||
* The object is completely controlled by lua.
|
||||
*
|
||||
* > ship = ship:Create(ship_id)
|
||||
*
|
||||
* Parameters:
|
||||
*
|
||||
* ship_id - The internal id of the ship type.
|
||||
*
|
||||
* Result:
|
||||
*
|
||||
* ship - new ship object
|
||||
*/
|
||||
static int l_ship_create(lua_State *l)
|
||||
{
|
||||
auto name = LuaPull<std::string>(l, 1);
|
||||
LuaObject<Ship>::CreateInLua(name);
|
||||
return 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* Method: GetInvulnerable
|
||||
*
|
||||
|
@ -1474,7 +1536,8 @@ static int l_ship_get_current_ai_command(lua_State *l)
|
|||
LuaPush(l, EnumStrings::GetString("ShipAICmdName", cmd->GetType()));
|
||||
return 1;
|
||||
} else {
|
||||
return 0;
|
||||
lua_pushnil(l);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1563,6 +1626,7 @@ void LuaObject<Ship>::RegisterClass()
|
|||
|
||||
{ "UseECM", l_ship_use_ecm },
|
||||
{ "IsECMReady", l_ship_is_ecm_ready },
|
||||
{ "GetDurationForDistance", l_ship_get_duration_for_distance },
|
||||
|
||||
{ "GetDockedWith", l_ship_get_docked_with },
|
||||
{ "Undock", l_ship_undock },
|
||||
|
@ -1582,6 +1646,8 @@ void LuaObject<Ship>::RegisterClass()
|
|||
{ "InitiateHyperjumpTo", l_ship_initiate_hyperjump_to },
|
||||
{ "AbortHyperjump", l_ship_abort_hyperjump },
|
||||
|
||||
{ "Create", l_ship_create },
|
||||
|
||||
{ "GetInvulnerable", l_ship_get_invulnerable },
|
||||
{ "SetInvulnerable", l_ship_set_invulnerable },
|
||||
|
||||
|
|
|
@ -242,7 +242,7 @@ static double t1_from_S1_mixed(val S1, val V0, val EV, val m, val F, val acap,
|
|||
}
|
||||
}
|
||||
|
||||
#define PRECALCPATH_TESTMODE
|
||||
//#define PRECALCPATH_TESTMODE
|
||||
#ifdef PRECALCPATH_TESTMODE
|
||||
enum class TestMode {
|
||||
CONSTRUCTOR,
|
||||
|
|
Loading…
Reference in New Issue