{$MODE OBJFPC} { -*- delphi -*- } {$INCLUDE settings.inc} unit materialbus; interface {$DEFINE VERBOSE} uses systems, internals, serverstream, materials, time, hashtable, genericutils, isdprotocol, plasticarrays, commonbuses, systemdynasty, masses, isdnumbers, annotatedpointer; type TMaterialBusFeatureNode = class; TStallReason = (srInput, srOutput); IMaterialPile = interface ['IMaterialPile'] function GetMaterialPileMaterial(): TMaterial; function GetMaterialPileCapacity(): TQuantity64; // quantity procedure SetMaterialPileMaterialBus(MaterialBus: TMaterialBusFeatureNode); procedure MaterialBusAdjustedMaterialPiles(); procedure DisconnectMaterialPile(); function GetDynasty(): TDynasty; end; TRegisterMaterialPileBusMessage = specialize TRegisterProviderBusMessage; TMaterialPileList = specialize PlasticArray; IFactory = interface ['IFactory'] function GetFactoryInputs(): TMaterialQuantity32Array; function GetFactoryOutputs(): TMaterialQuantity32Array; function GetFactoryRate(): TIterationsRate; // instances (not units!) per second; starts returning zero if stalled procedure SetFactoryMaterialBus(MaterialBus: TMaterialBusFeatureNode); procedure StartFactory(); procedure StallFactory(Reason: TStallReason); // disconnects the factory as well procedure DisconnectFactory(); function GetDynasty(): TDynasty; function GetPendingFraction(): PFraction32; end; FactoryUtils = specialize DefaultUnorderedUtils; TRegisterFactoryBusMessage = specialize TRegisterProviderBusMessage; TFactoryList = specialize PlasticArray; IMaterialConsumer = interface ['IMaterialConsumer'] // Consumers grab material as fast as possible, and only register when the piles are empty. // They might unregister later in a time slice than the material bus processes them, in which case GetMaterialConsumerMaterial() can return nil. function GetMaterialConsumerMaterial(): TMaterial; function GetMaterialConsumerMaxDelivery(): TQuantity32; function GetMaterialConsumerCurrentSchedule(): TQuantity64Schedule; // returns the value set by StartMaterialConsumer procedure SetMaterialConsumerMaterialBus(MaterialBus: TMaterialBusFeatureNode); procedure StartMaterialConsumer(ActualRate: TQuantity64Schedule); // only called if GetMaterialConsumerMaterial returns non-nil procedure DeliverMaterialConsumer(Delivery: TQuantity64); // 0 <= Delivery <= GetMaterialConsumerMaxDelivery; will always be called when syncing if StartMaterialConsumer was called procedure DisconnectMaterialConsumer(); // region is going away function GetDynasty(): TDynasty; {$IFOPT C+} function GetAsset(): TAssetNode; {$ENDIF} end; TRegisterMaterialConsumerBusMessage = specialize TRegisterProviderBusMessage; TMaterialConsumerList = specialize PlasticArray; TObtainMaterialBusMessage = class(TPhysicalConnectionBusMessage) strict private FDynasty: TDynasty; FRequest: TMaterialQuantity64; FDelivery: TQuantity32; function GetRemainingQuantity(): TQuantity32; inline; function GetFulfilled(): Boolean; inline; function GetTransferredManifest(): TMaterialQuantity64; inline; public constructor Create(ADynasty: TDynasty; ARequest: TMaterialQuantity64); overload; constructor Create(ADynasty: TDynasty; AMaterial: TMaterial; AQuantity: TQuantity32); overload; procedure Deliver(ADelivery: TQuantity32); property Dynasty: TDynasty read FDynasty; property Material: TMaterial read FRequest.Material; property Quantity: TQuantity32 read GetRemainingQuantity; property Fulfilled: Boolean read GetFulfilled; property TransferredManifest: TMaterialQuantity64 read GetTransferredManifest; end; TStoreMaterialBusMessage = class(TPhysicalConnectionWithExclusionBusMessage) strict private FDynasty: TDynasty; FRequest: TMaterialQuantity64; FStored: TQuantity64; function GetRemainingQuantity(): TQuantity64; inline; function GetFulfilled(): Boolean; inline; function GetTransferredManifest(): TMaterialQuantity64; inline; public constructor Create(AAsset: TAssetNode; ADynasty: TDynasty; ARequest: TMaterialQuantity64); overload; constructor Create(AAsset: TAssetNode; ADynasty: TDynasty; AMaterial: TMaterial; AQuantity: TQuantity64); overload; procedure Store(ADelivery: TQuantity64); property Dynasty: TDynasty read FDynasty; property Material: TMaterial read FRequest.Material; property RemainingQuantity: TQuantity64 read GetRemainingQuantity; property Fulfilled: Boolean read GetFulfilled; property TransferredManifest: TMaterialQuantity64 read GetTransferredManifest; end; TMaterialBusFeatureClass = class(TFeatureClass) strict protected function GetFeatureNodeClass(): FeatureNodeReference; override; public constructor Create(); constructor CreateFromTechnologyTree(const Reader: TTechTreeReader); override; function InitFeatureNode(ASystem: TSystem): TFeatureNode; override; end; TMaterialBusFeatureNode = class(TFeatureNode) private type PPerDynastyData = ^TPerDynastyData; TPerDynastyData = record FFactories: TFactoryList; // plastic array FMaterialConsumers: TMaterialConsumerList; // plastic array FMaterialPiles: TMaterialPileList; // plastic array FMaterialPileComposition: TMaterialQuantityHashTable; // allocated on demand FMaterialPileRateSchedules: TMaterialRateScheduleHashTable; class operator Initialize(var Rec: TPerDynastyData); // fills the record with zeroes class operator Finalize(var Rec: TPerDynastyData); // frees the allocated substructures procedure IncMaterialPile(Material: TMaterial; Delta: TQuantity64); procedure DecMaterialPile(Material: TMaterial; Delta: TQuantity64); function ClampedDecMaterialPile(Material: TMaterial; Delta: TQuantity64): TQuantity64; // returns how much was actually transferred end; TDynastyTable = specialize THashTable; TMaterialBusData = record strict private class operator Initialize(var Rec: TMaterialBusData); class operator Finalize(var Rec: TMaterialBusData); function GetData(Dynasty: TDynasty): PPerDynastyData; public type TDynastyMode = (dmNone, dmOne, dmMany); TDynastyEnumerator = class private FDynasty: TDynasty; FEnumerator: TDynastyTable.TKeyEnumerator; FStarted: Boolean; function GetCurrent(): TDynasty; public destructor Destroy(); override; function MoveNext(): Boolean; property Current: TDynasty read GetCurrent; function GetEnumerator(): TDynastyEnumerator; inline; end; private function GetDynastyEnumerator(): TDynastyEnumerator; function GetDynastyMode(): TDynastyMode; inline; function GetSingleDynastyData(): PPerDynastyData; inline; public function HasDynasty(Dynasty: TDynasty): Boolean; inline; property Data[Dynasty: TDynasty]: PPerDynastyData read GetData; default; property Dynasties: TDynastyEnumerator read GetDynastyEnumerator; property DynastyMode: TDynastyMode read GetDynastyMode; property SingleDynastyData: PPerDynastyData read GetSingleDynastyData; strict private const MultiDynastyMarker = $0001; var FDynasty: TDynasty; case TDynastyMode of dmNone: (FNil: Pointer); // no dynasty (FDynasty is nil) dmOne: (FSingleDynastyData: PPerDynastyData); // (FDynasty is a pointer) dmMany: (FDynastyTable: TDynastyTable); // (FDynasty = MultiDynastyMarker) end; // {$IF SIZEOF(TMaterialBusData) <> SIZEOF(Pointer) * 2} {$FATAL} {$ENDIF} private FAnchorTime: TTimeInMilliseconds; // set to Low(FAnchorTime) or Now when transfers are currently synced FNextEvent: TSystemEvent; // set only when mass is moving FActive: Boolean; // set to true when transfers are set up, set to false when transfers need to be set up FDynamic: Boolean; // set to true when the situation is dynamic (i.e. Sync() would do something) FData: TMaterialBusData; {$IFOPT C+} Busy: Boolean; {$ENDIF} // set to true while running our algorithms, to make sure nobody calls us reentrantly function GetTotalMaterialPileQuantity(Dynasty: TDynasty; Material: TMaterial): TQuantity64; function GetTotalMaterialPileQuantityFlowRate(Dynasty: TDynasty; Material: TMaterial): TQuantity64Schedule; function GetTotalMaterialPileCapacity(Dynasty: TDynasty; Material: TMaterial): TQuantity64; protected constructor CreateFromJournal(Journal: TJournalReader; AFeatureClass: TFeatureClass; ASystem: TSystem); override; procedure Attaching(); override; procedure Detaching(); override; function ManageBusMessage(Message: TBusMessage): TInjectBusMessageResult; override; function HandleBusMessage(Message: TBusMessage): THandleBusMessageResult; override; procedure Sync(); // move the materials around procedure SyncAndReconsider(); // sync, cancel the current scheduled event, schedule HandleChanges procedure HandleScheduledEvent(var Data); // same as SyncAndReconsider, but used as event handler procedure HandleChanges(); override; public constructor Create(ASystem: TSystem; AFeatureClass: TMaterialBusFeatureClass); destructor Destroy(); override; procedure UpdateJournal(Journal: TJournalWriter); override; procedure ApplyJournal(Journal: TJournalReader); override; procedure RemoveMaterialPile(MaterialPile: IMaterialPile); procedure RemoveFactory(Factory: IFactory); procedure RemoveMaterialConsumer(MaterialConsumer: IMaterialConsumer); procedure ClientChanged(); inline; // call when a material consumer thinks it might be done, or another client has changed rate; marks the region as dirty so everything gets recomputed function ExtractMaterialPile(MaterialPile: IMaterialPile): TQuantity64; // removes the pile and its contents; returns the quantity of material the pile had. function RehomeMaterialPile(MaterialPile: IMaterialPile): TQuantity64; // removes the pile, and if there isn't enough remaining capacity, returns the quantity remaining. function GetMaterialPileQuantity(Pile: IMaterialPile): TQuantity64; function GetMaterialPileQuantityFlowRate(Pile: IMaterialPile): TQuantity64Schedule; end; implementation uses sysutils, planetary, exceptions, hashfunctions, rubble, knowledge, ttparser, binaryheap; function FactoryHash32(const Key: IFactory): DWord; begin Result := PtrUIntHash32(PtrUInt(Key)); end; constructor TObtainMaterialBusMessage.Create(ADynasty: TDynasty; ARequest: TMaterialQuantity64); begin inherited Create(); Assert(Assigned(ADynasty)); Assert(Assigned(ARequest.Material)); FDynasty := ADynasty; FRequest := ARequest; Assert(not Fulfilled); end; constructor TObtainMaterialBusMessage.Create(ADynasty: TDynasty; AMaterial: TMaterial; AQuantity: TQuantity32); begin inherited Create(); Assert(Assigned(ADynasty)); Assert(Assigned(AMaterial)); FDynasty := ADynasty; FRequest.Material := AMaterial; FRequest.Quantity := AQuantity; Assert(not Fulfilled); end; function TObtainMaterialBusMessage.GetRemainingQuantity(): TQuantity32; begin Result := TQuantity32.FromQuantity64(FRequest.Quantity - FDelivery); // $R- end; function TObtainMaterialBusMessage.GetFulfilled(): Boolean; begin Result := FDelivery >= FRequest.Quantity; end; function TObtainMaterialBusMessage.GetTransferredManifest(): TMaterialQuantity64; begin if (FDelivery.IsPositive) then begin Result.Material := FRequest.Material; Result.Quantity := FDelivery; end else begin Result.Material := nil; Result.Quantity := TQuantity64.Zero; end; end; procedure TObtainMaterialBusMessage.Deliver(ADelivery: TQuantity32); begin Assert(FDelivery + ADelivery <= FRequest.Quantity); FDelivery := FDelivery + ADelivery; end; constructor TStoreMaterialBusMessage.Create(AAsset: TAssetNode; ADynasty: TDynasty; ARequest: TMaterialQuantity64); begin inherited Create(AAsset); Assert(Assigned(ADynasty)); Assert(Assigned(ARequest.Material)); FDynasty := ADynasty; FRequest := ARequest; Assert(FRequest.Quantity.IsPositive); Assert(not Fulfilled); end; constructor TStoreMaterialBusMessage.Create(AAsset: TAssetNode; ADynasty: TDynasty; AMaterial: TMaterial; AQuantity: TQuantity64); begin inherited Create(AAsset); Assert(Assigned(ADynasty)); Assert(Assigned(AMaterial)); FDynasty := ADynasty; FRequest.Material := AMaterial; FRequest.Quantity := AQuantity; Assert(FRequest.Quantity.IsPositive); Assert(not Fulfilled); end; function TStoreMaterialBusMessage.GetRemainingQuantity(): TQuantity64; begin Result := FRequest.Quantity - FStored; // $R- end; function TStoreMaterialBusMessage.GetFulfilled(): Boolean; begin Result := FStored >= FRequest.Quantity; end; function TStoreMaterialBusMessage.GetTransferredManifest(): TMaterialQuantity64; begin if (FStored.IsPositive) then begin Result.Material := FRequest.Material; Result.Quantity := FStored; end else begin Result.Material := nil; Result.Quantity := TQuantity64.Zero; end; end; procedure TStoreMaterialBusMessage.Store(ADelivery: TQuantity64); begin Assert(FStored + ADelivery <= FRequest.Quantity); FStored := FStored + ADelivery; end; constructor TMaterialBusFeatureClass.Create(); begin inherited Create(); end; constructor TMaterialBusFeatureClass.CreateFromTechnologyTree(const Reader: TTechTreeReader); begin inherited Create(); end; function TMaterialBusFeatureClass.GetFeatureNodeClass(): FeatureNodeReference; begin Result := TMaterialBusFeatureNode; end; function TMaterialBusFeatureClass.InitFeatureNode(ASystem: TSystem): TFeatureNode; begin Result := TMaterialBusFeatureNode.Create(ASystem, Self); end; class operator TMaterialBusFeatureNode.TPerDynastyData.Initialize(var Rec: TPerDynastyData); begin FillChar(Rec, SizeOf(Rec), 0); Assert(PPtrUInt(@Rec.FFactories)^ = 0); Assert(PPtrUInt(@Rec.FMaterialConsumers)^ = 0); Assert(PPtrUInt(@Rec.FMaterialPiles)^ = 0); Assert(not Assigned(Rec.FMaterialPileComposition)); Assert(not Assigned(Rec.FMaterialPileRateSchedules)); // if we do anything else here, see GetData below end; class operator TMaterialBusFeatureNode.TPerDynastyData.Finalize(var Rec: TPerDynastyData); begin FreeAndNil(Rec.FMaterialPileComposition); FreeAndNil(Rec.FMaterialPileRateSchedules); end; procedure TMaterialBusFeatureNode.TPerDynastyData.IncMaterialPile(Material: TMaterial; Delta: TQuantity64); begin Assert(Assigned(Material)); Assert(Delta.IsPositive); if (not Assigned(FMaterialPileComposition)) then begin FMaterialPileComposition := TMaterialQuantityHashTable.Create(1); end; FMaterialPileComposition.Inc(Material, Delta); end; procedure TMaterialBusFeatureNode.TPerDynastyData.DecMaterialPile(Material: TMaterial; Delta: TQuantity64); begin Assert(Assigned(Material)); Assert(Delta.IsPositive); Assert(Assigned(FMaterialPileComposition)); FMaterialPileComposition.Dec(Material, Delta); end; function TMaterialBusFeatureNode.TPerDynastyData.ClampedDecMaterialPile(Material: TMaterial; Delta: TQuantity64): TQuantity64; begin // We return how much was _actually_ transferred. Assert(Assigned(Material)); Assert(Delta.IsPositive); if (Assigned(FMaterialPileComposition)) then begin Result := FMaterialPileComposition.ClampedDec(Material, Delta); end else begin Result := TQuantity64.Zero; end; end; class operator TMaterialBusFeatureNode.TMaterialBusData.Initialize(var Rec: TMaterialBusData); begin Rec.FDynasty := nil; Rec.FNil := nil; end; class operator TMaterialBusFeatureNode.TMaterialBusData.Finalize(var Rec: TMaterialBusData); begin if (Assigned(Rec.FDynasty)) then begin if (PtrUInt(Rec.FDynasty) = MultiDynastyMarker) then begin FreeAndNil(Rec.FDynastyTable); end else begin Finalize(Rec.FSingleDynastyData^); FreeMem(Rec.FSingleDynastyData); end; end; end; function TMaterialBusFeatureNode.TMaterialBusData.GetData(Dynasty: TDynasty): PPerDynastyData; var OldData, NewData: PPerDynastyData; begin Assert(Assigned(Dynasty)); if (FDynasty = Dynasty) then begin Result := FSingleDynastyData; end else if (PtrUInt(FDynasty) = MultiDynastyMarker) then begin Result := FDynastyTable.GetOrAddPtr(Dynasty); end else if (not Assigned(FDynasty)) then begin FDynasty := Dynasty; GetMem(FSingleDynastyData, SizeOf(TPerDynastyData)); Initialize(FSingleDynastyData^); Result := FSingleDynastyData; end else begin OldData := FSingleDynastyData; FDynastyTable := TDynastyTable.Create(@DynastyHash32); NewData := FDynastyTable.ItemsPtr[FDynasty]; Move(OldData^, NewData^, SizeOf(TPerDynastyData)); // this is safe only because at this point we've guaranteed all those target bytes are zero, so there's nothing to deallocate FreeMem(OldData); PtrUInt(FDynasty) := MultiDynastyMarker; Result := FDynastyTable.GetOrAddPtr(Dynasty); end; end; function TMaterialBusFeatureNode.TMaterialBusData.GetDynastyEnumerator(): TDynastyEnumerator; begin // TODO: find a more efficient way of doing this in the common case of n=0 or n=1 // we really shouldn't need to allocate anything, or set up exception handlers, or do function calls if (not Assigned(FDynasty)) then begin Result := nil; end else begin Result := TDynastyEnumerator.Create(); if (PtrUInt(FDynasty) <> MultiDynastyMarker) then begin Result.FDynasty := FDynasty; end else begin Result.FEnumerator := FDynastyTable.GetEnumerator(); end; end; end; function TMaterialBusFeatureNode.TMaterialBusData.GetDynastyMode(): TDynastyMode; begin if (not Assigned(FDynasty)) then begin Result := dmNone; end else if (PtrUInt(FDynasty) <> MultiDynastyMarker) then begin Result := dmOne; end else begin Result := dmMany; end; end; function TMaterialBusFeatureNode.TMaterialBusData.GetSingleDynastyData(): PPerDynastyData; begin Assert(Assigned(FDynasty) and (PtrUInt(FDynasty) <> MultiDynastyMarker)); Result := FSingleDynastyData; end; function TMaterialBusFeatureNode.TMaterialBusData.HasDynasty(Dynasty: TDynasty): Boolean; begin if (not Assigned(FDynasty)) then begin Result := False; end else if (PtrUInt(FDynasty) <> MultiDynastyMarker) then begin Result := Dynasty = FDynasty; end else begin Result := FDynastyTable.Has(Dynasty); end; end; destructor TMaterialBusFeatureNode.TMaterialBusData.TDynastyEnumerator.Destroy(); begin FreeAndNil(FEnumerator); inherited; end; function TMaterialBusFeatureNode.TMaterialBusData.TDynastyEnumerator.GetCurrent(): TDynasty; begin if (Assigned(FDynasty)) then begin Result := FDynasty; end else begin Result := FEnumerator.Current; end; end; function TMaterialBusFeatureNode.TMaterialBusData.TDynastyEnumerator.MoveNext(): Boolean; begin if (Assigned(FDynasty)) then begin Result := not FStarted; FStarted := True; end else begin Result := FEnumerator.MoveNext(); end; end; function TMaterialBusFeatureNode.TMaterialBusData.TDynastyEnumerator.GetEnumerator(): TDynastyEnumerator; begin Result := Self; end; constructor TMaterialBusFeatureNode.Create(ASystem: TSystem; AFeatureClass: TMaterialBusFeatureClass); begin inherited Create(ASystem); FAnchorTime := TTimeInMilliseconds.NegInfinity; Assert(not FActive); Assert(not FDynamic); end; constructor TMaterialBusFeatureNode.CreateFromJournal(Journal: TJournalReader; AFeatureClass: TFeatureClass; ASystem: TSystem); begin inherited; FAnchorTime := TTimeInMilliseconds.NegInfinity; Assert(not FActive); Assert(not FDynamic); end; destructor TMaterialBusFeatureNode.Destroy(); begin if (FActive) then Detaching(); inherited; end; procedure TMaterialBusFeatureNode.Attaching(); begin MarkAsDirty([dkNeedsHandleChanges]); end; procedure TMaterialBusFeatureNode.Detaching(); procedure Reset(); inline; var MaterialPile: IMaterialPile; Factory: IFactory; MaterialConsumer: IMaterialConsumer; Dynasty: TDynasty; DynastyData: PPerDynastyData; begin Assert(not Assigned(FNextEvent)); // caller is responsible for canceling everything Assert(FActive); for Dynasty in FData.Dynasties do begin DynastyData := FData[Dynasty]; if (DynastyData^.FMaterialPiles.IsNotEmpty) then begin for MaterialPile in DynastyData^.FMaterialPiles.Without(nil) do MaterialPile.DisconnectMaterialPile(); DynastyData^.FMaterialPiles.Empty(); end; if (DynastyData^.FFactories.IsNotEmpty) then begin for Factory in DynastyData^.FFactories.Without(nil) do Factory.DisconnectFactory(); DynastyData^.FFactories.Empty(); end; if (DynastyData^.FMaterialConsumers.IsNotEmpty) then begin for MaterialConsumer in DynastyData^.FMaterialConsumers.Without(nil) do MaterialConsumer.DisconnectMaterialConsumer(); DynastyData^.FMaterialConsumers.Empty(); end; end; FActive := False; FDynamic := False; FAnchorTime := TTimeInMilliseconds.NegInfinity; end; begin if (FActive) then begin if (Assigned(FNextEvent)) then CancelEvent(FNextEvent); FDynamic := False; Reset(); Assert(not FActive); end; end; function TMaterialBusFeatureNode.GetTotalMaterialPileQuantity(Dynasty: TDynasty; Material: TMaterial): TQuantity64; var DynastyData: PPerDynastyData; begin Assert(FData.HasDynasty(Dynasty)); DynastyData := FData[Dynasty]; if (not Assigned(DynastyData^.FMaterialPileComposition)) then begin Result := TQuantity64.Zero; end else if (not DynastyData^.FMaterialPileComposition.Has(Material)) then begin Result := TQuantity64.Zero; end else begin Result := DynastyData^.FMaterialPileComposition[Material]; end; Assert(Result <= GetTotalMaterialPileCapacity(Dynasty, Material), 'recorded pile composition (' + Result.ToString() + ') exceeds pile capacity (' + GetTotalMaterialPileCapacity(Dynasty, Material).ToString() + ') for ' + Material.Name); if (FDynamic) then begin Result := Result + GetTotalMaterialPileQuantityFlowRate(Dynasty, Material).Evaluate(System.Now - FAnchorTime); Assert(Result <= GetTotalMaterialPileCapacity(Dynasty, Material), 'dynasty ' + IntToStr(Dynasty.DynastyID) + ': dynamic pile composition (' + Result.ToString() + ') exceeds pile capacity (' + GetTotalMaterialPileCapacity(Dynasty, Material).ToString() + ') for ' + Material.Name); end; end; function TMaterialBusFeatureNode.GetTotalMaterialPileQuantityFlowRate(Dynasty: TDynasty; Material: TMaterial): TQuantity64Schedule; var DynastyData: PPerDynastyData; begin if (FData.HasDynasty(Dynasty)) then begin DynastyData := FData[Dynasty]; if (DynastyData^.FMaterialPileRateSchedules.Has(Material)) then begin Result := DynastyData^.FMaterialPileRateSchedules[Material]; exit; end; end; Result.Reset(); end; function TMaterialBusFeatureNode.GetTotalMaterialPileCapacity(Dynasty: TDynasty; Material: TMaterial): TQuantity64; var Pile: IMaterialPile; DynastyData: PPerDynastyData; begin Assert(FData.HasDynasty(Dynasty)); DynastyData := FData[Dynasty]; Result := TQuantity64.Zero; if (DynastyData^.FMaterialPiles.IsNotEmpty) then begin for Pile in DynastyData^.FMaterialPiles.Without(nil) do begin if (Pile.GetMaterialPileMaterial() = Material) then Result := Result + Pile.GetMaterialPileCapacity(); end; end; end; function TMaterialBusFeatureNode.ManageBusMessage(Message: TBusMessage): TInjectBusMessageResult; begin if ((Message is TRegisterMaterialPileBusMessage) or (Message is TRegisterFactoryBusMessage) or (Message is TRegisterMaterialConsumerBusMessage) or (Message is TObtainMaterialBusMessage) or (Message is TStoreMaterialBusMessage)) then begin Result := DeferOrHandleBusMessage(Message); end else Result := irDeferred; end; function TMaterialBusFeatureNode.HandleBusMessage(Message: TBusMessage): THandleBusMessageResult; var MaterialPileMessage: TRegisterMaterialPileBusMessage; FactoryMessage: TRegisterFactoryBusMessage; MaterialConsumerMessage: TRegisterMaterialConsumerBusMessage; Obtain: TObtainMaterialBusMessage; Store: TStoreMaterialBusMessage; Capacity, CandidateDeliverySize, Usage: TQuantity64; DeliverySize: TQuantity32; MaterialPile: IMaterialPile; DynastyData: PPerDynastyData; Consumer: IMaterialConsumer; Material: TMaterial; begin {$IFOPT C+} Assert(not Busy); {$ENDIF} Assert(not ((Message is TRubbleCollectionMessage) or (Message is TDismantleMessage)), ClassName + ' should never see ' + Message.ClassName); if (Message is TRegisterMaterialPileBusMessage) then begin SyncAndReconsider(); MaterialPileMessage := Message as TRegisterMaterialPileBusMessage; DynastyData := FData[MaterialPileMessage.Provider.GetDynasty()]; Assert(not DynastyData^.FMaterialPiles.Contains(MaterialPileMessage.Provider)); DynastyData^.FMaterialPiles.Push(MaterialPileMessage.Provider); MaterialPileMessage.Provider.SetMaterialPileMaterialBus(Self); Result := hrHandled; end else if (Message is TRegisterFactoryBusMessage) then begin SyncAndReconsider(); FactoryMessage := Message as TRegisterFactoryBusMessage; DynastyData := FData[FactoryMessage.Provider.GetDynasty()]; Assert(not DynastyData^.FFactories.Contains(FactoryMessage.Provider)); DynastyData^.FFactories.Push(FactoryMessage.Provider); FactoryMessage.Provider.SetFactoryMaterialBus(Self); Result := hrHandled; end else if (Message is TRegisterMaterialConsumerBusMessage) then begin SyncAndReconsider(); MaterialConsumerMessage := Message as TRegisterMaterialConsumerBusMessage; DynastyData := FData[MaterialConsumerMessage.Provider.GetDynasty()]; Assert(not DynastyData^.FMaterialConsumers.Contains(MaterialConsumerMessage.Provider)); DynastyData^.FMaterialConsumers.Push(MaterialConsumerMessage.Provider); MaterialConsumerMessage.Provider.SetMaterialConsumerMaterialBus(Self); Result := hrHandled; // if we get here, they must have first tried to obtain all of this material Material := MaterialConsumerMessage.Provider.GetMaterialConsumerMaterial(); Assert((not Assigned(DynastyData^.FMaterialPileComposition)) or (not DynastyData^.FMaterialPileComposition.Has(Material)) or (DynastyData^.FMaterialPileComposition[Material].IsZero)); end else if (Message is TObtainMaterialBusMessage) then begin Writeln(DebugName, ' received ', Message.ClassName); Obtain := Message as TObtainMaterialBusMessage; if (FData.HasDynasty(Obtain.Dynasty)) then begin DynastyData := FData[Obtain.Dynasty]; Assert(Obtain.Quantity.IsPositive); SyncAndReconsider(); if (Assigned(DynastyData^.FMaterialPileComposition) and DynastyData^.FMaterialPileComposition.Has(Obtain.Material)) then begin CandidateDeliverySize := DynastyData^.FMaterialPileComposition[Obtain.Material]; if (CandidateDeliverySize.IsPositive) then begin if (CandidateDeliverySize > Obtain.Quantity) then DeliverySize := Obtain.Quantity else DeliverySize := TQuantity32.FromQuantity64(CandidateDeliverySize); Writeln(' delivering ', Obtain.Material.Name, ' - ', DeliverySize.ToString()); Obtain.Deliver(DeliverySize); DynastyData^.DecMaterialPile(Obtain.Material, DeliverySize); if (DynastyData^.FMaterialPiles.IsNotEmpty) then for MaterialPile in DynastyData^.FMaterialPiles.Without(nil) do if (MaterialPile.GetMaterialPileMaterial() = Obtain.Material) then MaterialPile.MaterialBusAdjustedMaterialPiles(); MarkAsDirty([dkUpdateJournal]); end; end; end; if (Obtain.Fulfilled) then Result := hrHandled else Result := inherited; end else if (Message is TStoreMaterialBusMessage) then begin Store := Message as TStoreMaterialBusMessage; Writeln(DebugName, ' received ', Store.ClassName, ' message for ', Store.RemainingQuantity.ToString(), ' of ', Store.Material.Name); if (FData.HasDynasty(Store.Dynasty)) then begin DynastyData := FData[Store.Dynasty]; Assert(Store.RemainingQuantity.IsPositive); SyncAndReconsider(); if (DynastyData^.FMaterialConsumers.IsNotEmpty) then begin for Consumer in DynastyData^.FMaterialConsumers.Without(nil) do begin if (Consumer.GetMaterialConsumerMaterial() = Store.Material) then begin Capacity := Consumer.GetMaterialConsumerMaxDelivery(); // this returns a 32 bit value if (Capacity > Store.RemainingQuantity) then begin // by definiton if we get here, Store.RemainingQuantity is smaller than some 32 bit value Capacity := Store.RemainingQuantity; end; if (Capacity.IsPositive) then begin // by definition if we get here, Capacity fits in a 32 bit value Assert(Capacity <= TQuantity32.Max); Writeln(' feeding ', Consumer.GetAsset().DebugName, ' a total of ', Capacity.ToString(), ' of ', Store.Material.Name); Consumer.DeliverMaterialConsumer(TQuantity32.FromQuantity64(Capacity)); Store.Store(Capacity); if (Store.Fulfilled) then break; end; end; end; Writeln(' after feeding consumers, we have ', Store.RemainingQuantity.ToString(), ' of ', Store.Material.Name, ' left to deal with'); end; if (Store.RemainingQuantity.IsPositive) then begin Capacity := GetTotalMaterialPileCapacity(Store.Dynasty, Store.Material); Writeln(' we have ', Capacity.ToString(), ' of total capacity for ', Store.Material.Name); if (Capacity.IsPositive) then begin if (Assigned(DynastyData^.FMaterialPileComposition) and DynastyData^.FMaterialPileComposition.Has(Store.Material)) then begin Usage := DynastyData^.FMaterialPileComposition[Store.Material]; if (Usage < Capacity) then begin Capacity := Capacity - Usage; end else begin Writeln(' (meaning we are completely full)'); Capacity := TQuantity64.Zero; end; end; if (Capacity.IsPositive) then begin Writeln(' we have ', Capacity.ToString(), ' of remaining capacity for ', Store.Material.Name); if (Capacity > Store.RemainingQuantity) then Capacity := Store.RemainingQuantity; Writeln(' taking ', Capacity.ToString()); Store.Store(Capacity); DynastyData^.IncMaterialPile(Store.Material, Capacity); if (DynastyData^.FMaterialPiles.IsNotEmpty) then for MaterialPile in DynastyData^.FMaterialPiles.Without(nil) do if (MaterialPile.GetMaterialPileMaterial() = Store.Material) then MaterialPile.MaterialBusAdjustedMaterialPiles(); MarkAsDirty([dkUpdateJournal]); end; end; end; end; if (Store.Fulfilled) then Result := hrHandled else Result := inherited; end else Result := inherited; end; procedure TMaterialBusFeatureNode.Sync(); var Dynasty: TDynasty; SyncDuration: TMillisecondsDuration; DynastyData: PPerDynastyData; Factory: IFactory; FactoryRate: TIterationsRate; MaterialConsumer: IMaterialConsumer; ConsumerSchedule: TQuantity64Schedule; MaterialPile: IMaterialPile; Iterations: Int64; Material: TMaterial; Quantity, MaxQuantity: TQuantity64; FactoryEntry: TMaterialQuantity32; begin Writeln(DebugName, ' :: Sync(Active=', FActive, '; Dynamic=', FDynamic, '; Now=', System.Now.ToString(), '; AnchorTime=', FAnchorTime.ToString(), ')'); Assert(FActive); if (not FDynamic) then begin Assert(not Assigned(FNextEvent)); exit; end; Writeln('SYNC FOR MATERIAL BUS'); SyncDuration := System.Now - FAnchorTime; Writeln(' duration: ', SyncDuration.ToString()); if (SyncDuration.IsZero) then exit; Assert(SyncDuration.IsPositive); {$IFOPT C+} Assert(not Busy); Busy := true; {$ENDIF} for Dynasty in FData.Dynasties do begin Writeln(' dynasty ', Dynasty.DynastyID); DynastyData := FData[Dynasty]; for Factory in DynastyData^.FFactories.Without(nil) do begin FactoryRate := Factory.GetFactoryRate(); Iterations := ApplyIncrementally(FactoryRate, SyncDuration, Factory.GetPendingFraction()^); for FactoryEntry in Factory.GetFactoryInputs() do begin Material := FactoryEntry.Material; Quantity := FactoryEntry.Quantity * Iterations; DynastyData^.DecMaterialPile(Material, Quantity); end; for FactoryEntry in Factory.GetFactoryOutputs() do begin Material := FactoryEntry.Material; Quantity := FactoryEntry.Quantity * Iterations; DynastyData^.IncMaterialPile(Material, Quantity); end; end; {$IFOPT C+} for Material in DynastyData^.FMaterialPileComposition do Assert(not DynastyData^.FMaterialPileComposition[Material].IsNegative); {$ENDIF} for MaterialConsumer in DynastyData^.FMaterialConsumers.Without(nil) do begin ConsumerSchedule := MaterialConsumer.GetMaterialConsumerCurrentSchedule(); Material := MaterialConsumer.GetMaterialConsumerMaterial(); Quantity := ConsumerSchedule.Evaluate(SyncDuration); MaxQuantity := MaterialConsumer.GetMaterialConsumerMaxDelivery(); if (Quantity > MaxQuantity) then Quantity := MaxQuantity; DynastyData^.DecMaterialPile(Material, Quantity); MaterialConsumer.DeliverMaterialConsumer(Quantity); if (MaterialConsumer.GetMaterialConsumerMaterial() = Material) then begin ConsumerSchedule.Advance(SyncDuration); MaterialConsumer.StartMaterialConsumer(ConsumerSchedule); end; end; {$IFOPT C+} for Material in DynastyData^.FMaterialPileComposition do Assert(not DynastyData^.FMaterialPileComposition[Material].IsNegative); {$ENDIF} if (Assigned(DynastyData^.FMaterialPileRateSchedules)) then begin DynastyData^.FMaterialPileRateSchedules.Advance(SyncDuration); end; if (DynastyData^.FMaterialPiles.IsNotEmpty) then begin for MaterialPile in DynastyData^.FMaterialPiles.Without(nil) do MaterialPile.MaterialBusAdjustedMaterialPiles(); end; Writeln(' end of dynasty ', Dynasty.DynastyID, ' region sync'); end; FAnchorTime := System.Now; {$IFOPT C+} Assert(Busy); Busy := False; {$ENDIF} end; procedure TMaterialBusFeatureNode.SyncAndReconsider(); begin if (FActive) then begin if (FDynamic) then begin Sync(); if (Assigned(FNextEvent)) then CancelEvent(FNextEvent); FAnchorTime := TTimeInMilliseconds.NegInfinity; end; FActive := False; FDynamic := False; end; MarkAsDirty([dkNeedsHandleChanges]); end; procedure TMaterialBusFeatureNode.HandleScheduledEvent(var Data); begin Writeln(DebugName, ' :: HandleScheduledEvent'); Assert(Assigned(FNextEvent)); Assert(FDynamic); // otherwise, why did we schedule an event FNextEvent := nil; // important to do this before anything that might raise an exception, otherwise we try to free it on exit Sync(); FActive := False; FDynamic := False; FAnchorTime := TTimeInMilliseconds.NegInfinity; MarkAsDirty([dkNeedsHandleChanges]); end; type TScheduleEntry = record strict private FFactory: IFactory; FTimeOrigin: TMillisecondsDuration; // initially -Period < TimeOrigin <= 0 FPeriod: Int32; // Period > 0 function GetPeriod(): TMillisecondsDuration; inline; function GetNextTime(): TMillisecondsDuration; inline; function GetIsFirstIteration(): Boolean; inline; public procedure Init(AFactory: IFactory; ATimeOrigin: TMillisecondsDuration; APeriod: TMillisecondsDuration); procedure Advance(); inline; // increments TimeOrigin by Period property Factory: IFactory read FFactory; property Period: TMillisecondsDuration read GetPeriod; property NextTime: TMillisecondsDuration read GetNextTime; property IsFirstIteration: Boolean read GetIsFirstIteration; end; TScheduleEntryMinHeapUtils = record class function IsOrdered(const A, B: TScheduleEntry): Boolean; static; inline; end; type TSchedule = specialize THeap; procedure TScheduleEntry.Init(AFactory: IFactory; ATimeOrigin: TMillisecondsDuration; APeriod: TMillisecondsDuration); begin Assert(APeriod.AsInt64 >= 0); Assert(APeriod.AsInt64 <= High(Int32)); Assert(not ATimeOrigin.IsPositive); FFactory := AFactory; FTimeOrigin := ATimeOrigin; FPeriod := APeriod.AsInt64; // $R- end; function TScheduleEntry.GetPeriod(): TMillisecondsDuration; begin Result := TMillisecondsDuration.FromMilliseconds(FPeriod); end; function TScheduleEntry.GetNextTime(): TMillisecondsDuration; begin Result := FTimeOrigin + TMillisecondsDuration.FromMilliseconds(FPeriod); end; function TScheduleEntry.GetIsFirstIteration(): Boolean; begin Result := not FTimeOrigin.IsPositive; end; procedure TScheduleEntry.Advance(); begin FTimeOrigin := FTimeOrigin + TMillisecondsDuration.FromMilliseconds(FPeriod); end; class function TScheduleEntryMinHeapUtils.IsOrdered(const A, B: TScheduleEntry): Boolean; var AT, BT: TMillisecondsDuration; begin AT := A.NextTime; BT := B.NextTime; if (AT > BT) then Result := True else Result := False; end; procedure TMaterialBusFeatureNode.HandleChanges(); var TimeUntilNextEvent: TMillisecondsDuration; Dynasty: TDynasty; DynastyData: PPerDynastyData; ScheduleBuilder: array of TScheduleEntry; Schedule: TSchedule; Period: TMillisecondsDuration; Fraction: Fraction32; MaterialConsumer: IMaterialConsumer; MaterialConsumerRequests: TMaterialQuantityHashTable; Material: TMaterial; TimeStep: TMillisecondsDuration; EntryPtr: ^TScheduleEntry; Index: Cardinal; Factory: IFactory; begin inherited; {$IFOPT C+} Assert(not Busy); Busy := True; {$ENDIF} if (not FActive) then begin Writeln(DebugName, ' recomputing material flow dynamics'); Writeln('MATERIAL BUS DYNAMICS'); Assert(not Assigned(FNextEvent)); Assert(not FDynamic); Assert(FAnchorTime.IsInfinite); TimeUntilNextEvent := TMillisecondsDuration.Infinity; for Dynasty in FData.Dynasties do begin DynastyData := FData[Dynasty]; Writeln('- dynasty ', Dynasty.DynastyID); DynastyData^.FFactories.RemoveAll(nil); SetLength(ScheduleBuilder, DynastyData^.FFactories.Length); Index := 0; for Factory in DynastyData^.FFactories do begin Period := Factory.GetFactoryRate.AsPeriod; Fraction := Factory.GetPendingFraction()^; ScheduleBuilder[Index].Init(Factory, -Period + Fraction * Period, Period); Inc(Index); end; Schedule.AdoptInit(ScheduleBuilder); DynastyData^.FMaterialConsumers.RemoveAll(nil); if (DynastyData^.FMaterialConsumers.Length > 0) then MaterialConsumerRequests := TMaterialQuantityHashTable.Create(); for MaterialConsumer in DynastyData^.FMaterialConsumers do begin Material := MaterialConsumer.GetMaterialConsumerMaterial(); if (Assigned(Material)) then MaterialConsumerRequests.Inc(Material, MaterialConsumer.GetMaterialConsumerMaxDelivery()); end; if (Schedule.Count > 0) then begin Schedule.PeekPtr() // keep pulling from the schedule, simulating the piles: // - compute everything at the time step // - if the first pull for a factory would hit a limit, then disable it // - if a subsequent pull would, then that's where we end // - process consumers at that time step // - if we would finish a consumer at that time step, then that's where we end // - at each step, build the schedule for piles for every material xxx end; end; if (FDynamic) then begin FAnchorTime := System.Now; if (not TimeUntilNextEvent.IsInfinite) then begin Assert(TimeUntilNextEvent.IsPositive); Assert(not Assigned(FNextEvent)); Writeln(' scheduling event for ', TimeUntilNextEvent.ToString()); FNextEvent := System.ScheduleEvent(TimeUntilNextEvent, @HandleScheduledEvent, Self); end; end else begin Writeln(' region is stable'); Assert(TimeUntilNextEvent.IsInfinite); end; FActive := True; end; {$IFOPT C+} Assert(Busy); Busy := False; {$ENDIF} Assert(FDynamic xor FAnchorTime.IsInfinite); end; procedure TMaterialBusFeatureNode.RemoveMaterialPile(MaterialPile: IMaterialPile); begin SyncAndReconsider(); FData[MaterialPile.GetDynasty()]^.FMaterialPiles.Replace(MaterialPile, nil); end; procedure TMaterialBusFeatureNode.RemoveFactory(Factory: IFactory); begin SyncAndReconsider(); FData[Factory.GetDynasty()]^.FFactories.Replace(Factory, nil); end; procedure TMaterialBusFeatureNode.RemoveMaterialConsumer(MaterialConsumer: IMaterialConsumer); begin SyncAndReconsider(); FData[MaterialConsumer.GetDynasty()]^.FMaterialConsumers.Replace(MaterialConsumer, nil); end; function TMaterialBusFeatureNode.ExtractMaterialPile(MaterialPile: IMaterialPile): TQuantity64; var Dynasty: TDynasty; DynastyData: PPerDynastyData; Material: TMaterial; PileRatio: Double; begin xxxxx SyncAndReconsider(); Assert(not FDynamic); Dynasty := MaterialPile.GetDynasty(); DynastyData := FData[Dynasty]; Material := MaterialPile.GetMaterialPileMaterial(); if (Assigned(DynastyData^.FMaterialPileComposition) and DynastyData^.FMaterialPileComposition.Has(Material)) then begin PileRatio := MaterialPile.GetMaterialPileCapacity() / GetTotalMaterialPileCapacity(Dynasty, Material); Result := GetTotalMaterialPileQuantity(Dynasty, Material) * PileRatio; DynastyData^.DecMaterialPile(Material, Result); end else Result := TQuantity64.Zero; DynastyData^.FMaterialPiles.Replace(MaterialPile, nil); MarkAsDirty([dkNeedsHandleChanges, dkUpdateClients, dkUpdateJournal]); end; function TMaterialBusFeatureNode.RehomeMaterialPile(MaterialPile: IMaterialPile): TQuantity64; var Dynasty: TDynasty; DynastyData: PPerDynastyData; Material: TMaterial; PileRatio: Double; PileCapacity, TotalCapacity, RemainingCapacity, RemainingQuantity, AffectedQuantity, TotalMaterialQuantity: TQuantity64; begin SyncAndReconsider(); xxxxx Assert(not FDynamic); Dynasty := MaterialPile.GetDynasty(); DynastyData := FData[Dynasty]; Material := MaterialPile.GetMaterialPileMaterial(); Writeln(' rehoming ', Material.Name); if (Assigned(DynastyData^.FMaterialPileComposition) and DynastyData^.FMaterialPileComposition.Has(Material)) then begin PileCapacity := MaterialPile.GetMaterialPileCapacity(); TotalCapacity := GetTotalMaterialPileCapacity(Dynasty, Material); Assert(PileCapacity <= TotalCapacity); PileRatio := PileCapacity / TotalCapacity; TotalMaterialQuantity := GetTotalMaterialPileQuantity(Dynasty, Material); AffectedQuantity := TotalMaterialQuantity * PileRatio; Assert(TotalMaterialQuantity >= AffectedQuantity); if (AffectedQuantity.IsPositive) then begin RemainingCapacity := TotalCapacity - PileCapacity; RemainingQuantity := TotalMaterialQuantity - AffectedQuantity; if (RemainingCapacity <= RemainingQuantity) then begin // nothing fits at all, we're completely full Result := AffectedQuantity; end else if (RemainingCapacity - RemainingQuantity < AffectedQuantity) then begin // we can't fit it all in the remaining capacity Result := AffectedQuantity - (RemainingCapacity - RemainingQuantity); end else begin Assert(TotalMaterialQuantity <= RemainingCapacity); // it all fits in the remaining capacity Result := TQuantity64.Zero; end; if (Result.IsPositive) then begin DynastyData^.DecMaterialPile(Material, Result); end; end else Result := TQuantity64.Zero; // the pile didn't have anything in it end else Result := TQuantity64.Zero; // we don't have any of that material DynastyData^.FMaterialPiles.Replace(MaterialPile, nil); MarkAsDirty([dkNeedsHandleChanges, dkUpdateClients, dkUpdateJournal]); end; procedure TMaterialBusFeatureNode.ClientChanged(); begin SyncAndReconsider(); end; function TMaterialBusFeatureNode.GetMaterialPileQuantity(Pile: IMaterialPile): TQuantity64; var Material: TMaterial; PileRatio: Double; Dynasty: TDynasty; begin xxxxx Dynasty := Pile.GetDynasty(); Material := Pile.GetMaterialPileMaterial(); PileRatio := Pile.GetMaterialPileCapacity() / GetTotalMaterialPileCapacity(Dynasty, Material); Result := GetTotalMaterialPileQuantity(Dynasty, Material) * PileRatio; end; function TMaterialBusFeatureNode.GetMaterialPileQuantityFlowRate(Pile: IMaterialPile): TQuantityRate; // units/s var Material: TMaterial; PileRatio: Double; Dynasty: TDynasty; begin xxxxxx Dynasty := Pile.GetDynasty(); Material := Pile.GetMaterialPileMaterial(); PileRatio := Pile.GetMaterialPileCapacity() / GetTotalMaterialPileCapacity(Dynasty, Material); Result := GetTotalMaterialPileQuantityFlowRate(Dynasty, Material) * PileRatio; end; procedure TMaterialBusFeatureNode.UpdateJournal(Journal: TJournalWriter); var Ore: TOres; Material: TMaterial; Dynasty: TDynasty; DynastyData: PPerDynastyData; begin for Dynasty in FData.Dynasties do begin Journal.WriteDynastyReference(Dynasty); DynastyData := FData[Dynasty]; if (Assigned(DynastyData^.FMaterialPileComposition)) then begin for Material in DynastyData^.FMaterialPileComposition do begin Journal.WriteMaterialReference(Material); Journal.WriteInt64(DynastyData^.FMaterialPileComposition[Material].AsInt64); end; end; Journal.WriteCardinal(0); // last material end; Journal.WriteCardinal(0); // last dynasty end; procedure TMaterialBusFeatureNode.ApplyJournal(Journal: TJournalReader); procedure ReadMaterials(var Materials: TMaterialQuantityHashTable); var Material: TMaterial; begin repeat Material := Journal.ReadMaterialReference(); if (Assigned(Material)) then begin if (not Assigned(Materials)) then Materials := TMaterialQuantityHashTable.Create(); Materials[Material] := TQuantity64.FromUnits(Journal.ReadInt64()); end; until not Assigned(Material); end; var Dynasty: TDynasty; DynastyData: PPerDynastyData; begin FAllocatedOres := Journal.ReadBoolean(); repeat Dynasty := Journal.ReadDynastyReference(); if (Assigned(Dynasty)) then begin DynastyData := FData[Dynasty]; ReadMaterials(DynastyData^.FMaterialPileComposition); end; until not Assigned(Dynasty); end; initialization RegisterFeatureClass(TMaterialBusFeatureClass); end.