/** Pump Pumps liquids using drain and source pipes. Features include: + switch on and off + consume/produce a variable amount of power depending on the height of source and drain @author Maikel, ST-DDT, Sven2, Newton */ #include Library_Structure #include Library_Ownable #include Library_PowerConsumer #include Library_PowerProducer #include Library_LiquidContainer #include Library_PipeControl #include Library_ResourceSelection static const PUMP_Menu_Action_Switch_On = "on"; static const PUMP_Menu_Action_Switch_Off = "off"; static const PUMP_Menu_Action_Description = "description"; local animation; // animation handle local switched_on; // controlled by Interaction. Indicates whether the user wants to pump or not local powered; // whether the pump has enough power as a consumer, always true if producing local power_used; // the amount of power currently consumed or (if negative) produced local clog_count; // increased when the pump doesn't find liquid or can't insert it. When it reaches max_clog_count, it will put the pump into temporary idle mode. local max_clog_count = 5; // note that even when max_clog_count is reached, the pump will search through offsets (but in idle mode) local accepted_mat; // currently accepted material. local stored_material_name; //contained liquid local stored_material_amount; /** This object is a liquid pump, thus pipes can be connected. */ public func IsLiquidPump() { return true; } public func IsLiquidContainer() { return false; } // The pump is rather complex for players. If anything happened, tell it to the player via the interaction menu. local last_status_message; public func Construction() { // Rotate at a 45 degree angle towards viewer and add a litte bit of Random this.MeshTransformation = Trans_Rotate(50 + RandomX(-10, 10), 0, 1, 0); // Default selection accepted_mat = nil; return _inherited(...); } public func IsHammerBuildable() { return true; } public func NoConstructionFlip() { return true; } public func Initialize() { switched_on = true; var start = 0; var end = GetAnimationLength("pump"); animation = PlayAnimation("pump", 5, Anim_Linear(GetAnimationPosition(animation), start, end, 35, ANIM_Loop)); SetState("Wait"); // Let the fast pump rule know it has been created. for (var rule in FindObjects(Find_ID(Rule_FastPump))) rule->~OnPumpCreation(this); return _inherited(...); } /*-- Interaction --*/ public func HasInteractionMenu() { return true; } public func GetPumpControlMenuEntries(object clonk) { var menu_entries = []; // default design of a control menu item var custom_entry = { Right = "100%", Bottom = "2em", BackgroundColor = {Std = 0, OnHover = 0x50ff0000}, image = {Right = "2em"}, text = {Left = "2em"} }; // Add info message about what is going on with the pump. var status = "$StateOk$"; var lightbulb_graphics = "Green"; if (last_status_message != nil) { status = last_status_message; lightbulb_graphics = "Red"; } PushBack(menu_entries, {symbol = this, extra_data = PUMP_Menu_Action_Description, custom = { Prototype = custom_entry, Bottom = "1.2em", Priority = -1, BackgroundColor = RGB(25, 100, 100), text = {Prototype = custom_entry.text, Text = status}, image = {Prototype = custom_entry.image, Symbol = Icon_Lightbulb, GraphicsName = lightbulb_graphics} }}); // switch on and off if (!switched_on) PushBack(menu_entries, GetPumpMenuEntry(custom_entry, Icon_Play, "$MsgTurnOn$", 1, PUMP_Menu_Action_Switch_On)); else PushBack(menu_entries, GetPumpMenuEntry(custom_entry, Icon_Stop, "$MsgTurnOff$", 1, PUMP_Menu_Action_Switch_Off)); return menu_entries; } public func GetPumpMenuEntry(proplist custom_entry, symbol, string text, int priority, extra_data) { return {symbol = symbol, extra_data = extra_data, custom = { Prototype = custom_entry, Priority = priority, text = {Prototype = custom_entry.text, Text = text}, image = {Prototype = custom_entry.image, Symbol = symbol} }}; } public func GetInteractionMenus(object clonk) { var menus = _inherited(clonk, ...) ?? []; var control_menu = { title = "$Control$", entries_callback = this.GetPumpControlMenuEntries, callback = "OnPumpControl", callback_hover = "OnPumpControlHover", callback_target = this, BackgroundColor = RGB(0, 50, 50), Priority = 20 }; PushBack(menus, control_menu); var materials_menu = { title = "$PumpMaterials$", entries_callback = this.GetPumpMaterialsMenuEntries, callback = "OnPumpMaterials", callback_hover = "OnPumpMaterialsHover", callback_target = this, BackgroundColor = RGB(0, 50, 50), Priority = 25 }; PushBack(menus, materials_menu); return menus; } public func OnPumpControlHover(id symbol, string action, desc_menu_target, menu_id) { var text = ""; if (action == PUMP_Menu_Action_Switch_On) text = "$DescTurnOn$"; else if (action == PUMP_Menu_Action_Switch_Off) text = "$DescTurnOff$"; else if (action == PUMP_Menu_Action_Description) text = this.Description; GuiUpdateText(text, menu_id, 1, desc_menu_target); } public func OnPumpControl(symbol_or_object, string action, bool alt) { if (action == PUMP_Menu_Action_Switch_On || action == PUMP_Menu_Action_Switch_Off) ToggleOnOff(true); UpdateInteractionMenus(this.GetPumpControlMenuEntries); } private func SetInfoMessage(string msg) { if (last_status_message == msg) return; last_status_message = msg; UpdateInteractionMenus(this.GetPumpControlMenuEntries); } static const LIBRARY_TANK_Menu_Action_Cut_AirPipe = "cutairpipe"; static const LIBRARY_TANK_Menu_Action_Add_AirPipe = "addairpipe"; // Extend liquid tank connection interface to air pipes. public func GetPipeControlMenuEntries(object clonk) { var menu_entries = inherited(clonk, ...); var drain_pipe = GetDrainPipe(); // Modify cut drain pipe menu entry for air pipe. if (drain_pipe && !drain_pipe->QueryCutLineConnection(this) && drain_pipe->IsAirPipe()) { for (var entry in menu_entries) { if (entry.extra_data == LIBRARY_TANK_Menu_Action_Cut_Drain) { entry.extra_data = LIBRARY_TANK_Menu_Action_Cut_AirPipe; entry.Priority = 4; entry.custom.image.BackgroundColor = RGB(0, 153, 255); entry.custom.text.Text = GetConnectedPipeMessage("$MsgCutAirPipe$", GetDrainPipe()); break; } } } // Add attach air pipe menu entry. var air_pipe = FindAvailablePipe(clonk, Find_Func("IsAirPipe")); if (!drain_pipe && air_pipe) PushBack(menu_entries, GetTankMenuEntry(air_pipe, GetConnectedPipeMessage("$MsgConnectAirPipe$", air_pipe), 4, LIBRARY_TANK_Menu_Action_Add_AirPipe, RGB(0, 153, 255))); return menu_entries; } public func OnPipeControlHover(symbol_or_object, string action, desc_menu_target, menu_id) { if (action == LIBRARY_TANK_Menu_Action_Cut_AirPipe) { GuiUpdateText(GetConnectedPipeDescription("$DescCutAirPipe$", GetDrainPipe()), menu_id, 1, desc_menu_target); return; } if (action == LIBRARY_TANK_Menu_Action_Add_AirPipe) { GuiUpdateText(GetConnectedPipeDescription("$DescConnectAirPipe$", symbol_or_object), menu_id, 1, desc_menu_target); return; } return inherited(symbol_or_object, action, desc_menu_target, menu_id, ...); } public func OnPipeControl(symbol_or_object, string action, object clonk) { if (action == LIBRARY_TANK_Menu_Action_Cut_AirPipe) { this->DoCutPipe(GetDrainPipe(), clonk); UpdateInteractionMenus(this.GetPipeControlMenuEntries); return; } if (action == LIBRARY_TANK_Menu_Action_Add_AirPipe) { this->DoConnectPipe(symbol_or_object, PIPE_STATE_Air); UpdateInteractionMenus(this.GetPipeControlMenuEntries); return; } return inherited(symbol_or_object, action, clonk, ...); } /*-- Pipe control --*/ public func QueryConnectPipe(object pipe, bool do_msg) { if (GetDrainPipe() && GetSourcePipe()) { if (do_msg) pipe->Report("$MsgHasPipes$"); return true; } else if (pipe->IsSourcePipe() && GetSourcePipe()) { if (do_msg) pipe->Report("$MsgSourcePipeProhibited$"); return true; } else if (pipe->IsDrainPipe() && GetDrainPipe()) { if (do_msg) pipe->Report("$MsgDrainPipeProhibited$"); return true; } else if (pipe->IsAirPipe() && GetDrainPipe()) { if (do_msg) pipe->Report("$MsgAirPipeProhibited$"); return true; } return false; } public func OnPipeConnect(object pipe, string specific_pipe_state) { if (PIPE_STATE_Source == specific_pipe_state) { SetSourcePipe(pipe); pipe->SetSourcePipe(); pipe->Report("$MsgCreatedSource$"); } else if (PIPE_STATE_Drain == specific_pipe_state) { SetDrainPipe(pipe); pipe->SetDrainPipe(); pipe->Report("$MsgCreatedDrain$"); } else if (PIPE_STATE_Air == specific_pipe_state) { // Air pipes take up the place of the drain SetDrainPipe(pipe); pipe->Report("$MsgCreatedAirDrain$"); } else { // add a drain if we already connected a source pipe, // or if the line is already connected to a container var line = pipe->GetConnectedLine(); var pump_target = !line || line->GetConnectedObject(this); if (pump_target) pump_target = pump_target->~IsLiquidContainer(); if (line->IsAirPipe()) { OnPipeConnect(pipe, PIPE_STATE_Air); } else if (GetSourcePipe() || pump_target) { OnPipeConnect(pipe, PIPE_STATE_Drain); } // otherwise create a source first else { OnPipeConnect(pipe, PIPE_STATE_Source); } } } public func OnPipeDisconnect(object pipe) { _inherited(pipe, ...); // Stop pumping to prevent errors from Pumping() function. if (pipe->IsAirPipe()) CheckState(); } public func SetSourcePipe(object pipe) { _inherited(pipe, ...); CheckState(); } public func IsAirPipeConnected() { if (!GetDrainPipe()) return false; return GetDrainPipe()->~IsAirPipe(); } /*-- Power stuff --*/ public func GetConsumerPriority() { if (IsAirPipeConnected()) return 125; return 25; } public func GetProducerPriority() { return 100; } public func IsSteadyPowerProducer() { return true; } public func OnNotEnoughPower() { powered = false; CheckState(); return _inherited(...); } public func OnEnoughPower() { powered = true; CheckState(); return _inherited(...); } // TODO: these functions may be useful in the liquid tank, maybe move it to that library /** Returns object to which the liquid is pumped */ private func GetDrainObject() { var drain = GetDrainPipe(); if (drain && drain->GetConnectedLine()) return drain->GetConnectedLine()->GetConnectedObject(this) ?? this; return this; } /** Returns object from which the liquid is pumped */ private func GetSourceObject() { var source = GetSourcePipe(); if (source && source->GetConnectedLine()) return source->GetConnectedLine()->GetConnectedObject(this) ?? this; return this; } /** PhaseCall of Pump: Pump the liquid from the source to the drain pipe */ protected func Pumping() { // at this point we can assert that we have power // something went wrong in the meantime? // let the central function handle that on next check if (!GetSourcePipe() && !IsAirPipeConnected()) return; // Get the drain object. var drain_obj = GetDrainObject(); // Don't do anything special if pumping air but inform the drain object. if (IsAirPipeConnected()) { if (!GetAirSourceOk() || !GetAirDrainOk()) return SetState("WaitForLiquid"); if (drain_obj) drain_obj->~OnAirPumped(this); return; } var pump_ok = true; // is empty? -> try to get liquid if (!stored_material_name) { // get new materials var source_obj = GetSourceObject(); var mat = this->ExtractMaterialFromSource(source_obj, this.PumpSpeed / 10); // no material to pump? if (mat) { stored_material_name = mat[0]; stored_material_amount = mat[1]; } else { source_obj->~CycleApertureOffset(this); // try different offsets, so we don't stop pumping just because 1px of earth was dropped on the source pipe pump_ok = false; } } if (pump_ok) { var i = stored_material_amount; while (i > 0) { if (this->InsertMaterialAtDrain(drain_obj, stored_material_name, 1)) { i--; } // Drain is stuck. else { drain_obj->~CycleApertureOffset(this); // try different offsets, so we don't stop pumping just because 1px of earth was dropped on the drain pipe pump_ok = false; break; } } stored_material_amount = i; if (stored_material_amount <= 0) stored_material_name = nil; } if (pump_ok) { clog_count = 0; } else { // Put into wait state if no liquid could be pumped for a while or if the drain has no aperture, i.e. is a liquid tank. if (++clog_count >= max_clog_count || !drain_obj->~HasAperture()) { SetState("WaitForLiquid"); } } return; } // interface for the extraction logic func ExtractMaterialFromSource(object source_obj, int amount) { if (source_obj->~IsLiquidContainer()) { return source_obj->RemoveLiquid(accepted_mat, amount, this); } else { var mat = source_obj->ExtractLiquidAmount(source_obj.ApertureOffsetX, source_obj.ApertureOffsetY, amount, true); if (mat) return [MaterialName(mat[0]), mat[1]]; } return nil; } // interface for the insertion logic public func InsertMaterialAtDrain(object drain_obj, string material_name, int amount) { // insert material into containers, if possible if (drain_obj->~IsLiquidContainer()) { amount -= drain_obj->PutLiquid(material_name, amount, this); } else { // convert to actual material, and insert remaining var material_index = Material(material_name); if (material_index == -1 && material_name != nil) material_index = Material(GetDefinition(material_name)->GetLiquidMaterial()); if (material_index != -1) { while (amount > 0 && drain_obj->InsertMaterial(material_index, drain_obj.ApertureOffsetX, drain_obj.ApertureOffsetY)) amount--; } } return amount <= 0; } /** Re check state and change the state if needed */ public func CheckState() { var is_fullcon = GetCon() >= 100; // The pump can work without source if it needs to supply air. var can_pump = (GetSourcePipe() || IsAirPipeConnected()) && is_fullcon && switched_on; // Can't pump at all -> wait. if (!can_pump) { if (!GetSourcePipe() && switched_on) SetInfoMessage("$StateNoSource$"); SetState("Wait"); } else if(IsAirPipeConnected()) { if (!GetAirSourceOk()) { SetInfoMessage("$StateNoAir$"); SetState("WaitForLiquid"); } else if (!GetAirDrainOk()) { SetInfoMessage("$StateNoAirNeed$"); SetState("WaitForLiquid"); } else { // Can pump, has air but has no power -> wait for power. if (!powered) { SetInfoMessage("$StateNoPower$"); SetState("WaitForPower"); } else { SetInfoMessage(); clog_count = 0; SetState("Pump"); } UpdatePowerUsage(); } } else { // Can pump but has no liquid or can't dispense liquid -> wait. var source_mat = GetLiquidSourceMaterial(); var source_ok = IsInResourceSelection(source_mat); var drain_ok = GetLiquidDrainOk(source_mat); if (!source_ok || !drain_ok) { accepted_mat = nil; if (!source_ok) SetInfoMessage("$StateNoInput$"); else if (!drain_ok) SetInfoMessage("$StateNoOutput$"); SetState("WaitForLiquid"); } else { accepted_mat = source_mat; // can pump, has liquid but has no power -> wait for power if (!powered) { SetInfoMessage("$StateNoPower$"); SetState("WaitForPower"); } // otherwise, pump! :-) else { SetInfoMessage(); clog_count = 0; SetState("Pump"); } // regularly update the power usage while pumping or waiting for power UpdatePowerUsage(); } } } /** Get current height the pump has to push liquids upwards (input.y - output.y) */ private func GetPumpHeight() { // compare each the surfaces of the bodies of liquid pumped // find Y position of surface of liquid that is pumped to target var source_obj = GetSourceObject(); var source_x = source_obj.ApertureOffsetX; var source_y = source_obj.ApertureOffsetY; if (source_obj->GBackLiquid(source_x, source_y)) { var src_mat = source_obj->GetMaterial(source_x, source_y); while (src_mat == source_obj->GetMaterial(source_x, source_y - 1)) --source_y; } // same for target (use same function as if inserting) var target_pos = {X = 0, Y = 0}; var drain_obj = GetDrainObject(); drain_obj->CanInsertMaterial(Material("Water"), drain_obj.ApertureOffsetX, drain_obj.ApertureOffsetY, target_pos); return source_obj->GetY() + source_y - target_pos.Y; } /** Recheck power usage/production for current pump height and make the pump a producer / consumer for the power system */ private func UpdatePowerUsage() { var new_power; if (IsUsingPower()) new_power = PumpHeight2Power(GetPumpHeight()); else new_power = 0; // do nothing if not necessary if (new_power == power_used) { // But still set powered to true if power_used was not positive. if (power_used <= 0) powered = true; return; } // and update energy system if (new_power > 0) { if (power_used < 0) { powered = false; // needed since the flag was set manually UnregisterPowerProduction(); } RegisterPowerRequest(new_power); } else if (new_power < 0) { if (power_used > 0) UnregisterPowerRequest(); RegisterPowerProduction(-new_power); powered = true; // when producing, we always have power } else // new_power == 0 { if (power_used < 0) UnregisterPowerProduction(); else if (power_used > 0) UnregisterPowerRequest(); powered = true; } power_used = new_power; return; } // Return whether the pump should be using power in the current state. private func IsUsingPower() { return switched_on && (GetAction() == "Pump" || GetAction() == "WaitForPower"); } // Transform pump height (input.y - output.y) into required power. private func PumpHeight2Power(int pump_height) { // Pumping upwards will always cost the minimum energy. // Pumping downwards will only produce energy after an offset. var power_offset = 10; // Max power consumed / produced. var max_power = 60; // Calculate the used power in steps of ten, every 60 pixels represents ten units. var used_power = pump_height / 60 * 10; // If the pump height is positive then add the minimum energy. if (pump_height >= 0) used_power = Min(used_power + 10, max_power); // Pumping power downwards never costs energy, but only brings something if offset is overcome. else used_power = BoundBy(used_power + power_offset - 10, -max_power, 0); // Pumped air always requires 20 power. if (IsAirPipeConnected()) used_power = 20; return used_power; } // Returns whether there is liquid at the source pipe to pump. private func GetLiquidSourceMaterial() { // Get the source object and check whether there is liquid. var source_obj = GetSourceObject(); if (!source_obj) return; // The source is a liquid container: check which material will be supplied. if (source_obj->~IsLiquidContainer()) { var liquid = source_obj->HasLiquid(GetResourceSelection()); if (liquid) return liquid->GetLiquidType(); return; } var is_liquid = source_obj->GBackLiquid(source_obj.ApertureOffsetX, source_obj.ApertureOffsetY); var liquid = MaterialName(source_obj->GetMaterial(source_obj.ApertureOffsetX, source_obj.ApertureOffsetY)); if (!is_liquid) { // Try different offsets, so we can resume pumping after clog because 1px of earth was dropped on the source pipe. source_obj->~CycleApertureOffset(this); return; } return liquid; } // Returns whether the drain pipe is free or the liquid container accepts the given material. private func GetLiquidDrainOk(string liquid) { if (!liquid) return false; var drain_obj = GetDrainObject(); if (drain_obj->~HasAperture()) { var material_index = Material(liquid); if (material_index == -1 && liquid != nil) material_index = Material(GetDefinition(liquid)->GetLiquidMaterial()); if (!drain_obj->CanInsertMaterial(material_index, drain_obj.ApertureOffsetX, drain_obj.ApertureOffsetY)) { drain_obj->~CycleApertureOffset(this); // try different offsets, so we can resume pumping after clog because 1px of earth was dropped on the source pipe return false; } } else if (drain_obj->~IsLiquidContainer()) { if (!drain_obj->AcceptsLiquid(liquid, 1)) return false; } return true; } // Returns whether the source (or alternatively the pump itself) is in free air. public func GetAirSourceOk() { var source_obj = GetSourceObject(); if (!source_obj) return !GBackSemiSolid(); var is_air = !source_obj->GBackSemiSolid(source_obj.ApertureOffsetX, source_obj.ApertureOffsetY); if (!is_air) source_obj->~CycleApertureOffset(this); return is_air; } // Returns whether the other side of air drain is in need of air. public func GetAirDrainOk() { var drain_obj = GetDrainObject(); if (!drain_obj) return false; return drain_obj->~QueryAirNeed(this); } // Set the state of the pump, retaining the animation position and updating the power usage. private func SetState(string act) { if (act == GetAction()) return; // Set animation depending on the current action. var start = 0; var end = GetAnimationLength("pump"); var anim_pos = GetAnimationPosition(animation); if (act == "Pump") { SetAnimationPosition(animation, Anim_Linear(anim_pos, start, end, 35, ANIM_Loop)); } else if(act == "WaitForLiquid") { SetAnimationPosition(animation, Anim_Linear(anim_pos, start, end, 350, ANIM_Loop)); } else { SetAnimationPosition(animation, Anim_Const(anim_pos)); } // Deactivate power usage when not pumping. if (powered && (act == "Wait" || act == "WaitForLiquid")) { if (power_used < 0) UnregisterPowerProduction(); else if (power_used > 0) UnregisterPowerRequest(); power_used = 0; powered = false; } // Finally, set the action. SetAction(act); } /* Deactivates a running pump or vice-versa. */ func ToggleOnOff(bool no_menu_refresh) { switched_on = !switched_on; CheckState(); if (!switched_on) SetInfoMessage("$StateTurnedOff$"); if (!no_menu_refresh) UpdateInteractionMenus(this.GetPumpControlMenuEntries); } /*-- Material Selection --*/ func IsResourceSelectionParent(id child_resource, id parent_resource) { return child_resource->~GetParentLiquidType() == parent_resource; } func ShowResourceSelectionMenuEntry(id resource) { return resource->~IsLiquid() && resource != Library_Liquid; } func InitResourceSelection() { // Add all liquids to the list of ones allowed to pump. var index = 0, resource; while (resource = GetDefinition(index++)) if (ShowResourceSelectionMenuEntry(resource)) AddToResourceSelection(resource); return inherited(); } func IsInResourceSelection(/* any */ mat) { if (GetType(mat) == C4V_Def) { return inherited(mat); } for (var resource in GetResourceSelection()) { if (resource->GetLiquidType() == mat) return true; } return false; } public func GetPumpMaterialsMenuEntries(object clonk) { return GetResourceSelectionMenuEntries(clonk); } public func OnPumpMaterialsHover(id symbol, string action, desc_menu_target, menu_id) { var text = ""; if (action == RESOURCE_SELECT_Menu_Action_Resource_Enable) { text = Format("$MsgEnableMaterial$", symbol->GetName()); } else if (action == RESOURCE_SELECT_Menu_Action_Resource_Disable) { text = Format("$MsgDisableMaterial$", symbol->GetName()); } GuiUpdateText(text, menu_id, 1, desc_menu_target); } public func OnPumpMaterials(symbol_or_object, string action, bool alt) { if (action == RESOURCE_SELECT_Menu_Action_Resource_Enable) { AddToResourceSelection(symbol_or_object); } else if (action == RESOURCE_SELECT_Menu_Action_Resource_Disable) { RemoveFromResourceSelection(symbol_or_object); } UpdateInteractionMenus(this.GetPumpMaterialsMenuEntries); } /*-- Properties --*/ protected func Definition(def) { // for title image SetProperty("PictureTransformation", Trans_Rotate(50, 0, 1, 0), def); // for building preview SetProperty("MeshTransformation", Trans_Rotate(50, 0, 1, 0), def); return _inherited(def, ...); } /* States "Wait": turned off or source pipe not connected "WaitForPower": turned on but no power (does consume power) "WaitForLiquid": turned on but no liquid (does not consume power) "Pump": currently working and consuming/producing power */ local ActMap = { Pump = { Prototype = Action, Name = "Pump", Length = 30, Delay = 3, Sound = "Structures::Pumpjack", NextAction = "Pump", StartCall = "CheckState", PhaseCall = "Pumping" }, Wait = { Prototype = Action, Name = "Wait", Delay = 90, NextAction = "Wait", EndCall = "CheckState" }, WaitForPower = { Prototype = Action, Name = "WaitForPower", Delay = 30, NextAction = "WaitForPower", EndCall = "CheckState" }, WaitForLiquid = { Prototype = Action, Name = "WaitForLiquid", Delay = 30, NextAction = "WaitForLiquid", EndCall = "CheckState" } }; local Name = "$Name$"; local Description = "$Description$"; local BlastIncinerate = 70; local NoBurnDecay = true; local HitPoints = 70; local FireproofContainer = true; local Components = {Wood = 1, Metal = 3}; // Pump speed in amount of pixels to pump per 30 frames. local PumpSpeed = 50;