Capture current source, content data, tests, asset licenses, and original-specification traceability. Original implementation remains incomplete. The accepted matching UI generation passed 154 suites and 12,253 checks, with eight rendered staff-contract screens reviewed. Latest native Windows and full-original acceptance remain outstanding.
254 lines
14 KiB
GDScript
254 lines
14 KiB
GDScript
class_name SupplyManagementService
|
|
extends RefCounted
|
|
|
|
## Player commands and deterministic replenishment. Vendor purchases arrive in
|
|
## the selected warehouse; all transfers inside the tree use real porter tasks.
|
|
|
|
func preview_purchase(runtime: Variant, warehouse_id: StringName, item_id: StringName, quantity: int) -> Dictionary:
|
|
var cash: int = runtime.economy_service.get_balance(&"PLAYER_CASH")
|
|
var item := SupplyCatalog.get_item(item_id)
|
|
var facility: FacilityInstance = runtime.facility_service.get_instance(warehouse_id)
|
|
var quote := {&"ok": false, &"reason": &"", &"total_cost": 0, &"cash_after": cash, &"quantity_after": 0, &"max_quantity": 0}
|
|
if facility == null or facility.definition_id != &"warehouse":
|
|
quote[&"reason"] = &"NOT_WAREHOUSE"
|
|
return quote
|
|
if item.is_empty() or not bool(item.get(&"purchasable", false)):
|
|
quote[&"reason"] = &"ITEM_NOT_PURCHASABLE"
|
|
return quote
|
|
var price := int(item[&"unit_price"])
|
|
var space: int = maxi(SupplyCatalog.storage_capacity(facility.definition_id) - SupplyCatalog.inventory_weight(runtime.inventory_service, facility.local_inventory_id) - _incoming_weight(runtime, facility.local_inventory_id), 0)
|
|
quote[&"max_quantity"] = mini(space / int(item[&"unit_weight"]), maxi(cash, 0) / price)
|
|
quote[&"quantity_after"] = runtime.inventory_service.total_quantity(facility.local_inventory_id, item_id) + maxi(quantity, 0)
|
|
quote[&"total_cost"] = maxi(quantity, 0) * price
|
|
quote[&"cash_after"] = cash - int(quote[&"total_cost"])
|
|
if quantity <= 0 or quantity > 10000:
|
|
quote[&"reason"] = &"INVALID_QUANTITY"
|
|
elif not facility.allows_logistics():
|
|
quote[&"reason"] = &"FACILITY_CLOSED"
|
|
elif int(quote[&"total_cost"]) > cash:
|
|
quote[&"reason"] = &"INSUFFICIENT_FUNDS"
|
|
elif quantity * int(item[&"unit_weight"]) > space:
|
|
quote[&"reason"] = &"STORAGE_FULL"
|
|
else:
|
|
quote[&"ok"] = true
|
|
return quote
|
|
|
|
|
|
func purchase(runtime: Variant, command: SimulationCommand) -> CommandResult:
|
|
var payload := command.payload
|
|
if not _text(payload.get(&"warehouse_id")) or not _text(payload.get(&"item_id")) or not _integer(payload.get(&"quantity")):
|
|
return _failure(command, &"INVALID_PAYLOAD")
|
|
var warehouse_id := StringName(payload[&"warehouse_id"])
|
|
var item_id := StringName(payload[&"item_id"])
|
|
var quantity := int(payload[&"quantity"])
|
|
var quote := preview_purchase(runtime, warehouse_id, item_id, quantity)
|
|
if not bool(quote[&"ok"]):
|
|
return _failure(command, StringName(quote[&"reason"]), quote)
|
|
var facility: FacilityInstance = runtime.facility_service.get_instance(warehouse_id)
|
|
var item := SupplyCatalog.get_item(item_id)
|
|
var minute: int = runtime._runtime_event_minute()
|
|
var inventory_before: Dictionary = runtime.inventory_service.create_snapshot()
|
|
var economy_before: Dictionary = runtime.economy_service.create_snapshot()
|
|
var stocked: Dictionary = runtime.inventory_service.add_stack(facility.local_inventory_id, item_id, quantity, 100, minute + int(item[&"shelf_life_minutes"]), minute, int(item[&"unit_weight"]))
|
|
if not bool(stocked.get(&"ok", false)):
|
|
return _failure(command, &"INVENTORY_ERROR")
|
|
var paid: CommandResult = runtime.economy_service.transfer(&"PLAYER_CASH", runtime.OPERATING_EXPENSE_ACCOUNT, int(quote[&"total_cost"]), minute, &"OPERATING_EXPENSE", warehouse_id, StringName("supply.purchase.%d" % command.command_id), &"supply_purchase", &"", false)
|
|
if not paid.is_success():
|
|
runtime.inventory_service.restore_snapshot(inventory_before)
|
|
runtime.economy_service.restore_snapshot(economy_before)
|
|
return _failure(command, &"PAYMENT_FAILED")
|
|
if not runtime.economy_service.commit_transaction_notification(StringName(paid.details[&"transaction_id"])):
|
|
runtime.inventory_service.restore_snapshot(inventory_before)
|
|
runtime.economy_service.restore_snapshot(economy_before)
|
|
return _failure(command, &"PAYMENT_FAILED")
|
|
quote.merge({&"warehouse_id": warehouse_id, &"item_id": item_id, &"quantity": quantity, &"transaction_id": paid.details[&"transaction_id"]})
|
|
return CommandResult.succeeded(command.command_id, quote)
|
|
|
|
|
|
func set_production(runtime: Variant, command: SimulationCommand) -> CommandResult:
|
|
var payload := command.payload
|
|
if not _text(payload.get(&"facility_id")) or not payload.get(&"enabled") is bool or not _integer(payload.get(&"target_stock")):
|
|
return _failure(command, &"INVALID_PAYLOAD")
|
|
var facility_id := StringName(payload[&"facility_id"])
|
|
var facility: FacilityInstance = runtime.facility_service.get_instance(facility_id)
|
|
if facility == null or facility.definition_id != &"kitchen":
|
|
return _failure(command, &"NOT_KITCHEN")
|
|
var result: Dictionary = runtime.kitchen_production.set_plan(facility_id, bool(payload[&"enabled"]), int(payload[&"target_stock"]))
|
|
if not bool(result.get(&"ok", false)):
|
|
return _failure(command, StringName(result.get(&"code", &"INVALID_TARGET")), result)
|
|
return CommandResult.succeeded(command.command_id, result)
|
|
|
|
|
|
func get_report(runtime: Variant) -> Dictionary:
|
|
var warehouses: Array[Dictionary] = []
|
|
var kitchens: Array[Dictionary] = []
|
|
var destinations: Array[Dictionary] = []
|
|
for facility: FacilityInstance in _facilities(runtime):
|
|
var row := _facility_row(runtime, facility)
|
|
match facility.definition_id:
|
|
&"warehouse":
|
|
var quote := preview_purchase(runtime, facility.instance_id, SupplyCatalog.RAW_FOOD, 1)
|
|
row[&"can_receive"] = bool(quote[&"ok"])
|
|
row[&"reason_codes"] = [] if bool(quote[&"ok"]) else [quote[&"reason"]]
|
|
warehouses.append(row)
|
|
&"kitchen":
|
|
row.merge(runtime.kitchen_production.get_report(facility.instance_id), true)
|
|
row.merge({&"target_min": 4, &"target_step": 4, &"target_max": SupplyCatalog.KITCHEN_TARGET_MAX, &"inputs": {SupplyCatalog.RAW_FOOD: 4}, &"outputs": {SupplyCatalog.BASIC_MEAL: 4}}, true)
|
|
row[&"reason_codes"] = [] if row[&"state"] in [&"PRODUCING", &"READY", &"TARGET_REACHED"] else [row[&"state"]]
|
|
kitchens.append(row)
|
|
&"cafeteria":
|
|
row[&"missing_items"] = {SupplyCatalog.BASIC_MEAL: maxi(12 - runtime.inventory_service.available_quantity(facility.local_inventory_id, SupplyCatalog.BASIC_MEAL), 0)}
|
|
row[&"reason_codes"] = facility.state_reasons.duplicate()
|
|
if runtime.inventory_service.available_quantity(facility.local_inventory_id, SupplyCatalog.BASIC_MEAL) <= 0:
|
|
row[&"reason_codes"].append(&"NO_INPUT")
|
|
destinations.append(row)
|
|
var bridge_report: Dictionary = runtime.supply_logistics.get_report()
|
|
return {&"cash": runtime.economy_service.get_balance(&"PLAYER_CASH"), &"catalog": SupplyCatalog.get_purchase_items(), &"warehouses": warehouses, &"kitchens": kitchens, &"destinations": destinations, &"deliveries": bridge_report.get(&"deliveries", [])}
|
|
|
|
|
|
func ensure_deliveries(runtime: Variant, minute: int) -> void:
|
|
var facilities := _facilities(runtime)
|
|
for destination: FacilityInstance in facilities:
|
|
if not destination.allows_logistics():
|
|
continue
|
|
var source_type: StringName
|
|
var item_id: StringName
|
|
var target := 0
|
|
if destination.definition_id == &"kitchen" and bool(runtime.kitchen_production.get_plan(destination.instance_id).get(&"enabled", false)):
|
|
source_type = &"warehouse"
|
|
item_id = SupplyCatalog.RAW_FOOD
|
|
target = SupplyCatalog.RAW_TARGET
|
|
elif destination.definition_id == &"cafeteria":
|
|
source_type = &"kitchen"
|
|
item_id = SupplyCatalog.BASIC_MEAL
|
|
target = 12
|
|
else:
|
|
continue
|
|
var deficit: int = target - runtime.inventory_service.total_quantity(destination.local_inventory_id, item_id) - _incoming_quantity(runtime, destination.local_inventory_id, item_id)
|
|
var space: int = SupplyCatalog.storage_capacity(destination.definition_id) - SupplyCatalog.inventory_weight(runtime.inventory_service, destination.local_inventory_id) - _incoming_weight(runtime, destination.local_inventory_id)
|
|
var needed: int = mini(mini(deficit, space), 4)
|
|
if needed <= 0:
|
|
continue
|
|
var sources: Array[Dictionary] = []
|
|
for source: FacilityInstance in facilities:
|
|
if source.definition_id != source_type or not source.allows_logistics():
|
|
continue
|
|
if source_type == &"kitchen" and not _has_production_plan(runtime, source.instance_id):
|
|
continue
|
|
var available: int = runtime.inventory_service.available_quantity(source.local_inventory_id, item_id, minute)
|
|
if available <= 0:
|
|
continue
|
|
sources.append({&"facility": source, &"available": available, &"distance": _connection_distance(runtime, source, destination)})
|
|
# Prefer a connected nearby supplier; IDs resolve equal routes stably.
|
|
# If none is connected, retain one visible request for route repair.
|
|
sources.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
|
|
if a[&"distance"] != b[&"distance"]: return a[&"distance"] < b[&"distance"]
|
|
return String(a[&"facility"].instance_id) < String(b[&"facility"].instance_id))
|
|
for source_row: Dictionary in sources:
|
|
var source: FacilityInstance = source_row[&"facility"]
|
|
var available := int(source_row[&"available"])
|
|
var ids_before: Dictionary = runtime.id_generator.create_snapshot()
|
|
var request_id := StringName("supply.%s" % runtime.id_generator.next_id(&"logistics_request"))
|
|
var result: CommandResult = runtime._execute_create_logistics_request(SimulationCommand.new(0, minute, minute, 50, &"create_logistics_request", &"supply", destination.instance_id, {&"request_id": request_id, &"source_inventory_id": source.local_inventory_id, &"destination_inventory_id": destination.local_inventory_id, &"item_requirements": {item_id: mini(needed, available)}, &"priority": 50}))
|
|
if not result.is_success():
|
|
runtime.id_generator.restore_snapshot(ids_before)
|
|
break
|
|
|
|
|
|
func validate_demolition(runtime: Variant, facility_id: StringName) -> Dictionary:
|
|
var facility: FacilityInstance = runtime.facility_service.get_instance(facility_id)
|
|
if facility == null:
|
|
return {&"ok": true}
|
|
if SupplyCatalog.inventory_weight(runtime.inventory_service, facility.local_inventory_id) > 0:
|
|
return {&"ok": false, &"reason": &"STOCK_REMAINS"}
|
|
for request: LogisticsRequest in runtime.logistics_requests.values():
|
|
if not request.is_terminal() and facility.local_inventory_id in [request.source_inventory_id, request.destination_inventory_id]:
|
|
return {&"ok": false, &"reason": &"DELIVERY_ACTIVE"}
|
|
if facility.definition_id == &"kitchen":
|
|
var report: Dictionary = runtime.kitchen_production.get_report(facility_id)
|
|
if bool(report.get(&"enabled", false)) or not report.get(&"active_batch", {}).is_empty():
|
|
return {&"ok": false, &"reason": &"PRODUCTION_ACTIVE"}
|
|
return {&"ok": true}
|
|
|
|
|
|
func _facility_row(runtime: Variant, facility: FacilityInstance) -> Dictionary:
|
|
var definition: FacilityDefinition = runtime.definition_registry.get_facility(facility.definition_id)
|
|
return {&"facility_id": facility.instance_id, &"display_name": definition.display_name, &"origin": facility.origin, &"layer": facility.origin.z, &"used": SupplyCatalog.inventory_weight(runtime.inventory_service, facility.local_inventory_id), &"capacity": SupplyCatalog.storage_capacity(facility.definition_id), &"stock": _stock(runtime, facility.local_inventory_id)}
|
|
|
|
|
|
func _stock(runtime: Variant, inventory_id: StringName) -> Array[Dictionary]:
|
|
var rows: Array[Dictionary] = []
|
|
for item_id: StringName in [SupplyCatalog.RAW_FOOD, SupplyCatalog.BASIC_MEAL]:
|
|
var total: int = runtime.inventory_service.total_quantity(inventory_id, item_id)
|
|
var available: int = runtime.inventory_service.available_quantity(inventory_id, item_id)
|
|
rows.append({&"item_id": item_id, &"total": total, &"available": available, &"reserved": total - available, &"in_transit": _incoming_quantity(runtime, inventory_id, item_id)})
|
|
return rows
|
|
|
|
|
|
func _incoming_quantity(runtime: Variant, inventory_id: StringName, item_id: StringName) -> int:
|
|
var quantity := 0
|
|
for request: LogisticsRequest in runtime.logistics_requests.values():
|
|
if request.is_terminal():
|
|
continue
|
|
if runtime.supply_logistics.is_returning(request.request_id):
|
|
if request.source_inventory_id == inventory_id:
|
|
quantity += runtime.inventory_service.total_quantity(StringName("cargo:%s" % request.assigned_carrier_id), item_id)
|
|
elif request.destination_inventory_id == inventory_id:
|
|
quantity += int(request.item_requirements.get(item_id, 0))
|
|
return quantity
|
|
|
|
|
|
func _incoming_weight(runtime: Variant, inventory_id: StringName) -> int:
|
|
var weight := 0
|
|
for request: LogisticsRequest in runtime.logistics_requests.values():
|
|
if request.is_terminal():
|
|
continue
|
|
if runtime.supply_logistics.is_returning(request.request_id):
|
|
if request.source_inventory_id == inventory_id:
|
|
weight += SupplyCatalog.inventory_weight(runtime.inventory_service, StringName("cargo:%s" % request.assigned_carrier_id))
|
|
elif request.destination_inventory_id == inventory_id:
|
|
weight += request.total_weight
|
|
return weight
|
|
|
|
|
|
func _facilities(runtime: Variant) -> Array[FacilityInstance]:
|
|
var rows: Array[FacilityInstance] = runtime.facility_service.get_instances()
|
|
rows.sort_custom(func(a: FacilityInstance, b: FacilityInstance) -> bool: return String(a.instance_id) < String(b.instance_id))
|
|
return rows
|
|
|
|
|
|
func _failure(command: SimulationCommand, reason: StringName, details: Dictionary = {}) -> CommandResult:
|
|
var result := details.duplicate(true)
|
|
result[&"reason"] = reason
|
|
return CommandResult.failed(CommandResult.Code.VALIDATION_FAILED, command.command_id, "Supply command rejected: %s" % reason, result)
|
|
|
|
|
|
func _text(value: Variant) -> bool:
|
|
return (value is String or value is StringName) and not String(value).is_empty()
|
|
|
|
|
|
func _integer(value: Variant) -> bool:
|
|
return value is int or (value is float and is_finite(value) and value == floor(value) and absf(value) <= 1000000.0)
|
|
|
|
|
|
func _has_production_plan(runtime: Variant, facility_id: StringName) -> bool:
|
|
for plan: Dictionary in runtime.kitchen_production.get_plans():
|
|
if StringName(plan[&"facility_id"]) == facility_id:
|
|
return true
|
|
return false
|
|
|
|
|
|
func _connection_distance(runtime: Variant, source: FacilityInstance, destination: FacilityInstance) -> int:
|
|
var distance := 1000000
|
|
var seen: Dictionary = {}
|
|
for cell: Vector3i in source.occupied_cells:
|
|
for delta: Vector3i in [Vector3i.ZERO, Vector3i.LEFT, Vector3i.RIGHT, Vector3i.UP, Vector3i.DOWN]:
|
|
var start := cell + delta
|
|
if seen.has(start) or not runtime.path_service.has_cell(start):
|
|
continue
|
|
seen[start] = true
|
|
var route: Dictionary = runtime.supply_logistics.find_facility_access_path(start, destination)
|
|
if bool(route.get(&"reachable", false)):
|
|
distance = mini(distance, int(route[&"steps"]))
|
|
return distance
|