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
Gliese852 2021-02-11 21:56:45 +03:00 committed by Webster Sheets
parent 31911813f8
commit 20551ae2ea
17 changed files with 2311 additions and 1004 deletions

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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")

View File

@ -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;
}
}

View File

@ -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;

View File

@ -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)

View File

@ -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 },

View File

@ -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 },

View File

@ -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,