Skip to content

Commit

Permalink
feature: Working trading! Details below...
Browse files Browse the repository at this point in the history
feature: Production/Consumption system - a ticker that simulates processing of Assets based on 'recipes'.
feature: Asset-Needs system - a ticker that updates Needs based on the amounts of affecting Assets (e.g. food for Hunger, money for Wealth).
feature: API to set Need updates batched to avoid repeated proc-calls.
feature: A DB of 'objective' Need weights to use as a reference point
tweak: Sell offers now consider their own Utility AND the 'objective' Utility for pricing and take the maximum of the two prices.
tweak: Sell offers now have 'friction' in the form of minimum money volume required to even bother (hardcoded at 100$ ATM).
refactor: Removed the special-case fetcher for Wealth - no longer needed due to A-N system.
refactor: Moved the Attachments variable to be common across all datums.
fix: Fixed routing of goods from escrow on successful contracts.
fix: Fixes to global ID lazy generation
  • Loading branch information
jmalek committed Sep 22, 2024
1 parent 1bed49a commit 2d50dd5
Show file tree
Hide file tree
Showing 28 changed files with 626 additions and 147 deletions.
Binary file modified GOAI/GOAI.dmb
Binary file not shown.
7 changes: 6 additions & 1 deletion GOAI/GOAI.dme
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
// BEGIN_INCLUDE
#include "__goai_library_features.dm"
#include "__spaceman_dmm.dm"
#include "_a_debug_mode.dm"
#include "_b_debug_modules.dm"
#include "_defines.dm"
#include "_need_keys.dm"
Expand Down Expand Up @@ -54,6 +55,7 @@
#include "_datastructures\tuple.dm"
#include "_datastructures\turfchunk.dm"
#include "_datastructures\vectors.dm"
#include "_datastructures\global_id_impls\faction_data_id.dm"
#include "_datastructures\registries\ai.dm"
#include "_datastructures\registries\aibrain.dm"
#include "_datastructures\registries\dynamic_query_cache.dm"
Expand All @@ -63,7 +65,9 @@
#include "_datastructures\registries\smartobjects.dm"
#include "_datastructures\registries\economy\assets.dm"
#include "_datastructures\registries\economy\last_trades.dm"
#include "_datastructures\registries\economy\market_utilities.dm"
#include "_datastructures\registries\economy\trade_offers.dm"
#include "basics\__datums.dm"
#include "basics\_cover.dm"
#include "basics\_dir_blocker.dm"
#include "basics\actions.dm"
Expand Down Expand Up @@ -123,6 +127,7 @@
#include "integrations\spawners\factions.dm"
#include "integrations\spawners\mob_commanders.dm"
#include "integrations\spawners\multiz.dm"
#include "integrations\trading\asset_needs_system.dm"
#include "integrations\trading\commodity.dm"
#include "integrations\trading\trade_contract.dm"
#include "integrations\trading\trade_offer.dm"
Expand Down Expand Up @@ -184,7 +189,7 @@
#include "integrations\utility_agent\senses\pathservice.dm"
#include "integrations\utility_agent\senses\vision.dm"
#include "integrations\utility_agent\systems\movement_system.dm"
#include "maps\combatmap_tabular_mini.dmm"
#include "maps\trademap2fac.dmm"
#include "utility\actions.dm"
#include "utility\actionset.dm"
#include "utility\actiontemplate.dm"
Expand Down
2 changes: 0 additions & 2 deletions GOAI/Z_testjunk/devmobs.dm
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@
icon = 'icons/uristmob/simpleanimals.dmi'
icon_state = "ANTAG"

var/dict/attachments

var/spawn_commander = TRUE
var/equip = TRUE
var/ensure_unique_name = FALSE
Expand Down
2 changes: 1 addition & 1 deletion GOAI/_b_debug_modules.dm
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
//# define DEBUG_UTILITY_MEMORY_QUERIES 0
//# define DEBUG_UTILITY_INPUT_FETCHERS 0
//# define PLANNING_CONSIDERATIONS_DEBUG_LOGGING 0
//# define MARKETWATCH_DEBUG_LOGGING 0
# define MARKETWATCH_DEBUG_LOGGING 0

#endif

Expand Down
6 changes: 4 additions & 2 deletions GOAI/_datastructures/global_id.dm
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
// as well as a unified API for setting that.
//
// This allows all global tables to reuse this ID as their key.
//
// The ID should be a string, because BYOND hashmaps be BYONDing.
*/

/datum
Expand All @@ -21,12 +23,12 @@
return src.global_id

// By default, generate an ID from a ref macro - guaranteed to be unique up to a reallocation.
var/default_id = ref(src)
var/default_id = "\ref[src]"
src.global_id = default_id
return default_id


// Shorthand for a very common code pattern where the Global ID is lazily initialized.
// The OR operator is short-circuiting, so the initialization will only be done once, when needed,
// because the initialization proc is expected to set the new ID on the object as part of its contract.
#define GET_GLOBAL_ID_LAZY(TargetDatum) (TargetDatum.global_id || TargetDatum.InitializeGlobalId())
#define GET_GLOBAL_ID_LAZY(TargetDatum) (isnull(TargetDatum.global_id) ? TargetDatum.InitializeGlobalId() : TargetDatum.global_id)
10 changes: 10 additions & 0 deletions GOAI/_datastructures/global_id_impls/faction_data_id.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@

/datum/faction_data/InitializeGlobalId(var/list/args = null)
if(src.global_id)
// Do not rewrite the ID if initialized
return src.global_id

// By default, generate an ID from the registry index stringified
var/default_id = "[src.registry_index]"
src.global_id = default_id
return default_id
220 changes: 215 additions & 5 deletions GOAI/_datastructures/registries/economy/assets.dm
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,224 @@
# ifdef GOAI_LIBRARY_FEATURES
var/global/list/global_asset_registry
# endif

# ifdef GOAI_SS13_SUPPORT
GLOBAL_LIST_EMPTY(global_asset_registry)
# endif

#define ASSETS_TABLE_LAZY_INIT(_Unused) if(isnull(GOAI_LIBBED_GLOB_ATTR(global_asset_registry)) || !islist(GOAI_LIBBED_GLOB_ATTR(global_asset_registry))) { GOAI_LIBBED_GLOB_ATTR(global_asset_registry) = list() }

#define HAS_REGISTERED_ASSETS(id) (id && GOAI_LIBBED_GLOB_ATTR(global_asset_registry) && (id in GOAI_LIBBED_GLOB_ATTR(global_asset_registry)))
#define CREATE_ASSETS_TRACKER(id) (GOAI_LIBBED_GLOB_ATTR(global_asset_registry)[id] = list())
#define CREATE_ASSETS_TRACKER_IF_NEEDED(id) if(!(id && HAS_REGISTERED_ASSETS(id))) { CREATE_ASSETS_TRACKER(id) };
#define GET_ASSETS_TRACKER(id) (GOAI_LIBBED_GLOB_ATTR(global_asset_registry)[id])
#define UPDATE_ASSETS_TRACKER(id, new_data) (GOAI_LIBBED_GLOB_ATTR(global_asset_registry)[id] = new_data)
#define HAS_REGISTERED_ASSETS(AssetId) (AssetId && GOAI_LIBBED_GLOB_ATTR(global_asset_registry) && (AssetId in GOAI_LIBBED_GLOB_ATTR(global_asset_registry)))
#define CREATE_ASSETS_TRACKER(AssetId) (GOAI_LIBBED_GLOB_ATTR(global_asset_registry)[AssetId] = list())
#define CREATE_ASSETS_TRACKER_IF_NEEDED(AssetId) if(!(AssetId && HAS_REGISTERED_ASSETS(AssetId))) { ASSETS_TABLE_LAZY_INIT(TRUE); CREATE_ASSETS_TRACKER(AssetId) };
#define GET_ASSETS_TRACKER(AssetId) (GOAI_LIBBED_GLOB_ATTR(global_asset_registry)[AssetId])
#define UPDATE_ASSETS_TRACKER(AssetId, NewData) (GOAI_LIBBED_GLOB_ATTR(global_asset_registry)[AssetId] = NewData)


// tracks the running subsystem, by ticker ID hash, to prevent duplication
var/global/production_subsystem_running = null

// Format-string to use to construct a unique hash for the Production/Consumption subsystem
// Currently uses a random number AND a world.time to ensure collisions are extremely unlikely
#define PRODUCTIONSYSTEM_TICKER_ID_HASH(MaxRand) "[rand(1, MaxRand)]-[world.time]"

// tracks the time delta for integrating the values
var/global/production_subsystem_last_update_time = null

// Inline version; generally preferable unless you REALLY need a proc
#define INITIALIZE_PRODUCTION_SYSTEM_INLINE(Tickrate) if(TRUE) {\
StartProductionConsumptionSystem(Tickrate); \
MARKETWATCH_DEBUG_LOG("Initialized a production/consumption subsystem with tickrate [DEFAULT_IF_NULL(Tickrate, DEFAULT_PRODUCTION_SYSTEM_TICKRATE)]"); \
};

// Variant - does the same, but only if it's not already initialized
#define INITIALIZE_PRODUCTION_SYSTEM_INLINE_IF_NEEDED(Tickrate) if(isnull(GOAI_LIBBED_GLOB_ATTR(production_subsystem_running))) {\
INITIALIZE_PRODUCTION_SYSTEM_INLINE(Tickrate); \
};

#define INITIALIZE_PRODUCTION_SYSTEM_INLINE_IF_NEEDED_AT_DEFAULT_RATE INITIALIZE_PRODUCTION_SYSTEM_INLINE_IF_NEEDED(null)

#define ECONOMY_ASSET_TRANSFORMS_DATA_FP GOAI_DATA_PATH("economy_asset_transforms.json")

/proc/StartProductionConsumptionSystem(var/tickrate = null, var/my_id = null)
/* Starts a backgrounded Production/Consumption system.
//
// This is something like the old-style SS13 Chemistry (simpler than 2020s-chem),
// except for living economy - modeling how capital transforms goods.
*/
set waitfor = FALSE

// Our ID; should be a unique string
var/ticker_id = my_id || PRODUCTIONSYSTEM_TICKER_ID_HASH(1000)
var/tick_rate = max(1, DEFAULT_IF_NULL(tickrate, DEFAULT_PRODUCTION_SYSTEM_TICKRATE))
GOAI_LIBBED_GLOB_ATTR(production_subsystem_running) = ticker_id

// Waitfor is false, so we use this sleep to detach the 'thread'
sleep(0)

// Initialize last update time if needed
if(isnull(GOAI_LIBBED_GLOB_ATTR(production_subsystem_last_update_time)))
GOAI_LIBBED_GLOB_ATTR(production_subsystem_last_update_time) = world.time

// We might want to try reloading this later in the loop, for now this is kinda useless
var/db_initialized = TRUE

// Load 'recipes' (what asset consumes and/or produces other assets) into an in-memory DB
var/list/prodconsume_db = READ_JSON_FILE(ECONOMY_ASSET_TRANSFORMS_DATA_FP)
if(!istype(prodconsume_db))
to_world_log("WARNING: Failed to load ECONOMY_ASSET_TRANSFORMS_DATA file at [ECONOMY_ASSET_TRANSFORMS_DATA_FP] - DB not initialized!")
db_initialized = FALSE
prodconsume_db = list()

// This is a ticker; it will continue to run while the global holds its ID
// If we start a new instance, it will take over the value in the global
// thereby terminating any old instances.
while(ticker_id == GOAI_LIBBED_GLOB_ATTR(production_subsystem_running))
// Wait for the next iteration
sleep(tick_rate)
to_world_log("= PRODUCTION/CONSUMPTION SYSTEM: STARTED TICK! =")

// Fix time to the start time of the tick
var/now = world.time

if((1 + now - GOAI_LIBBED_GLOB_ATTR(production_subsystem_last_update_time)) < PRODUCTIONSYSTEM_TICKSIZE_DSECONDS)
to_world_log("= PRODUCTION/CONSUMPTION SYSTEM: TICK ENDED EARLY - below quant =")
continue

if(!istype(GOAI_LIBBED_GLOB_ATTR(global_faction_registry)))
GOAI_LIBBED_GLOB_ATTR(global_faction_registry) = list()

for(var/faction_ref in GOAI_LIBBED_GLOB_ATTR(global_faction_registry))
// can yield between factions, assuming they have separate resource pools
sleep(0)

// might be a weakref, so we cannot typecast in the loop directly
var/datum/faction_data/faction = RESOLVE_PAWN(faction_ref)

if(!istype(faction))
to_world_log("= PRODUCTION/CONSUMPTION SYSTEM: SKIPPING FACTION for ref [NULL_TO_TEXT(faction_ref)] - wrong type! =")
continue

var/faction_id = GET_GLOBAL_ID_LAZY(faction)
var/list/actual_assets = GET_ASSETS_TRACKER(faction_id)
var/list/faction_assets = actual_assets?.Copy()

if(!faction_assets)
to_world_log("= PRODUCTION/CONSUMPTION SYSTEM: SKIPPING [faction.name]|ID=[faction_id] - no assets =")
continue

to_world_log("= PRODUCTION/CONSUMPTION SYSTEM: PROCESSING [faction.name]|ID=[faction_id] with assets: [json_encode(faction_assets)] =")

// How far back we are in the simulation
// We have already checked this is at least one quantum of production-tick in the past
// (IOW, we always have at least one whole simulation-tick to do if we got here at all)
var/curr_simulation_time = GOAI_LIBBED_GLOB_ATTR(production_subsystem_last_update_time)

while(curr_simulation_time < now)
// Simulate ticks iteratively - ensures everything has a chance to get produced evenly.
// We could integrate in one big chunk, and it would theoretically be faster, but would
// require untangling a horrible mess of conflicts if multiple sinks use the same inputs.

// NOTE: We need to do this all in one 'system' tick to maintain consistency
// otherwise assets might have a race condition issue.

// We'll go through all 'transforming' assets (e.g. foundry eats ore, produces steel) and apply the deltas.
// Overall schema is: {asset: {"consumes": {other_asset: delta}, "produces": {other_asset: delta}}}
// Consumed amounts are checked first; if any item would go into negatives, the whole transform is aborted.
curr_simulation_time += PRODUCTIONSYSTEM_TICKSIZE_DSECONDS

to_world_log("= PRODUCTION/CONSUMPTION SYSTEM: PROCESSING [faction.name] - simulation tick... =")

for(var/productive_asset_key in prodconsume_db)
// Go through all Things Wot Use/Produce Resources...
if(isnull(productive_asset_key))
continue

// ...check how much they use/produce stuff...
var/list/asset_deltas = prodconsume_db[productive_asset_key]

if(!asset_deltas)
continue

var/owned_amt = faction_assets[productive_asset_key]

if(isnull(owned_amt) || (owned_amt <= 0))
// We don't have it, no point processing
continue

// TODO: consider randomly skipping these based on low last update time
// to randomize who gets the first pick of resource consumption

// ...starting with inputs...
var/list/consumed_deltas = asset_deltas["consumes"]
var/consume_valid = TRUE
var/consumed_mult = PLUS_INF

// ...making sure all inputs are THERE...
for(var/checked_consumed_asset in consumed_deltas)
if(consumed_mult <= 0)
consume_valid = FALSE
break

if(isnull(checked_consumed_asset))
continue

var/raw_checked_delta = (consumed_deltas[checked_consumed_asset] || 0)

if(raw_checked_delta <= 0)
continue

var/assets_amt = (faction_assets[checked_consumed_asset] || 0)

if(assets_amt < raw_checked_delta)
consume_valid = FALSE
consumed_mult = 0
break

// Note: This is an approximation that might get buggy, but is super cheap.
// The more accurate method would be to do this in a loop over owned_amt,
// but that would be significantly slower at large scales.
// This is a fairly hot loop, so we can't afford that probably.
var/multiples = FLOOR(assets_amt / raw_checked_delta)
consumed_mult = min(multiples, consumed_mult, owned_amt)

if(!consume_valid)
// if something is missing, this specific process is skipped/aborted
continue

var/list/produced_deltas = asset_deltas["produces"]

var/list/faction_assets_backup = faction_assets.Copy()
var/valid_transaction = TRUE

// Subtract everything consumed
for(var/consumed_asset in consumed_deltas)
var/current_amt = faction_assets[consumed_asset]
var/consumed_amt = consumed_deltas[consumed_asset] * owned_amt
var/new_amt = (current_amt - consumed_amt)

if(new_amt < 0)
valid_transaction = FALSE
break

faction_assets[consumed_asset] = new_amt

if(!valid_transaction)
// rollback
faction_assets = faction_assets_backup.Copy()
continue

// Add everything produced
for(var/produced_asset in produced_deltas)
var/produced_amt = produced_deltas[produced_asset] * owned_amt
var/current_amt = (faction_assets[produced_asset] || 0)

faction_assets[produced_asset] = (current_amt + produced_amt)

UPDATE_ASSETS_TRACKER(faction_id, faction_assets)

// update the last check time to now
production_subsystem_last_update_time = now

return

40 changes: 40 additions & 0 deletions GOAI/_datastructures/registries/economy/market_utilities.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
// Stores the 'reference' Utilities for Needs.
//
// Various factions may value things higher or lower than these reference points.
// If it's lower, then they need to be able to use this reference point to price sale goods correctly,
// otherwise they will undercut themselves. This is particularly pertinent to produced goods.
//
// This is a bit of a hack, but you can handwave it as actors knowing the market more or less.
// It helps design-wise to provide a somewhat sane stable standard.
//
// You may wonder why can't we simply standardize default utilities to '1' or something.
// The answer is - not all goods have positive marginal Utility, so this is more flexible.
*/

# ifdef GOAI_LIBRARY_FEATURES
var/global/list/reference_market_utilities
# endif
# ifdef GOAI_SS13_SUPPORT
GLOBAL_LIST_EMPTY(reference_market_utilities)
# endif


#define DEFAULT_MARKET_UTILITY_DB_FP GOAI_DATA_PATH("reference_utilities.json")


/proc/InitReferenceUtilitiesDb(var/filepath_override = null, var/force = FALSE)
if(!(isnull(GOAI_LIBBED_GLOB_ATTR(reference_market_utilities)) || force))
return TRUE

var/db_filepath = isnull(filepath_override) ? DEFAULT_MARKET_UTILITY_DB_FP : filepath_override
READ_JSON_FILE_CACHED(db_filepath, GOAI_LIBBED_GLOB_ATTR(reference_market_utilities))
to_world_log("New ReferenceUtilitiesDB is [GOAI_LIBBED_GLOB_ATTR(reference_market_utilities) ? json_encode(GOAI_LIBBED_GLOB_ATTR(reference_market_utilities)) : "uninitialized"] from [db_filepath]")

if(!GOAI_LIBBED_GLOB_ATTR(reference_market_utilities))
GOAI_LIBBED_GLOB_ATTR(reference_market_utilities) = null

return TRUE


#define MARKET_UTILITIES_TABLE_LAZY_INIT(_Unused) if(isnull(GOAI_LIBBED_GLOB_ATTR(reference_market_utilities)) || !islist(GOAI_LIBBED_GLOB_ATTR(reference_market_utilities))) { InitReferenceUtilitiesDb() }
Loading

0 comments on commit 2d50dd5

Please sign in to comment.