/** Clonk use controls Author: Newton Objects that inherit this object need to return _inherited(...) in the following callbacks (if defined): Construction, Departure, Entrance, AttachTargetLost, CrewSelection, Death, Destruction The following callbacks are made to other objects: *Use, *UseStop, *UseStart, *UseHolding, *UseCancel Used properties this.control.current_object: object that is being used at the moment this.control.using_type: way of usage this.control.mlastx: last x position of the cursor this.control.mlasty: last y position of the cursor this.control.noholdingcallbacks: whether to do HoldingUseControl callbacks this.control.shelved_command: command (function) with condition that will be executed when the condition is met used for example to re-call *Use/Throw commands when the Clonk finished scaling wheras * is 'Contained' if the clonk is contained and otherwise (riding, pushing, to self) it is 'Control'. The item in the inventory only gets the Use*-calls. If the callback is handled, you should return true. Currently, this is explained more in detail here: http://forum.openclonk.org/topic_show.pl?tid=337 */ /* ++++++++++++++++++++++++ Callbacks ++++++++++++++++++++++++ */ protected func Construction() { if (this.control == nil) this.control = {}; this.control.current_object = nil; this.control.using_type = nil; this.control.shelved_command = nil; return _inherited(...); } public func GetUsedObject() { return this.control.current_object; } // The using-command hast to be canceled if the clonk is entered into // or exited from a building. protected func Entrance() { CancelUse(); return _inherited(...); } protected func Departure() { CancelUse(); return _inherited(...); } // The same for vehicles protected func AttachTargetLost() { CancelUse(); return _inherited(...); } // ...aaand the same for when the clonk is deselected protected func CrewSelection(bool unselect) { if (unselect) { // cancel usage on unselect first... CancelUse(); } return _inherited(unselect, ...); } protected func Destruction() { // cancel usage... CancelUse(); return _inherited(...); } protected func Death() { // cancel usage... CancelUse(); return _inherited(...); } /* ++++++++++++++++++++++++ Shelved command ++++++++++++++++++++++++ */ public func ShelveCommand(object condition_obj, string condition, object callback_obj, string callback, proplist data) { this.control.shelved_command = { cond = condition, cond_obj = condition_obj, callback = callback, callback_obj = callback_obj, data = data }; AddEffect("ShelvedCommand", this, 1, 5, this); } public func StopShelvedCommand() { this.control.shelved_command = nil; if (GetEffect("ShelvedCommand", this)) RemoveEffect("ShelvedCommand", this); } func FxShelvedCommandTimer(_, effect, time) { if (!this.control.shelved_command) return -1; if (!this.control.shelved_command.callback_obj) return -1; if (!this.control.shelved_command.cond_obj) return -1; if (!this.control.shelved_command.cond_obj->Call(this.control.shelved_command.cond, this.control.shelved_command.data)) return 1; this.control.shelved_command.callback_obj->Call(this.control.shelved_command.callback, this.control.shelved_command.data); return -1; } func FxShelvedCommandStop(target, effect, reason, temp) { if (temp) return; this.control.shelved_command = nil; } /* ++++++++++++++++++++++++ Use Controls ++++++++++++++++++++++++ */ public func CancelUse() { if (!GetUsedObject()) { // just forget any possibly stored actions StopShelvedCommand(); return; } // use the saved x,y coordinates for canceling CancelUseControl(this.control.mlastx, this.control.mlasty); } // to be called during usage of an object to re-start usage as soon as possible func PauseUse(object obj, string custom_condition, proplist data) { // cancel use first, since it removes old shelved commands if (this.control.started_use) { CancelUse(); this.control.started_use = false; } var callback_obj = this; if (custom_condition != nil) { callback_obj = obj; } else { custom_condition = "CanReIssueCommand"; } data = data ?? {}; data.obj = obj; data.ctrl = CON_Use; ShelveCommand(callback_obj, custom_condition, this, "ReIssueCommand", data); } func DetermineUsageType(object obj) { if (!obj) return nil; // house if (obj == Contained()) return C4D_Structure; // object if (obj->Contained() == this) return C4D_Object; // vehicle var proc = GetProcedure(); if (obj == GetActionTarget()) if (proc == "ATTACH" && proc == "PUSH") return C4D_Vehicle; // unknown return nil; } func GetUseCallString(string action) { // Control... or Contained... var control_string = "Control"; if (this.control.using_type == C4D_Structure) control_string = "Contained"; // ..Use.. or ..UseAlt... var estr = ""; if (this.control.alt && this.control.using_type != C4D_Object) estr = "Alt"; // Action if (!action) action = ""; return Format("~%sUse%s%s", control_string, estr, action); } func CanReIssueCommand(proplist data) { if (!data.obj) return false; if (data.ctrl == CON_Use) return !data.obj->~RejectUse(this); } func ReIssueCommand(proplist data) { if (data.ctrl == CON_Use) return StartUseControl(data.ctrl, this.control.mlastx, this.control.mlasty, data.obj); } func StartUseControl(int ctrl, int x, int y, object obj) { this.control.started_use = false; if (obj->~RejectUse(this)) { // remember for later: PauseUse(obj); // but still catch command return true; } // Disable climb/hangle actions for the duration of this use if (obj.ForceFreeHands && !GetEffect("IntControlFreeHands", this)) AddEffect("IntControlFreeHands", this, 130, 0, this); obj->SetController(GetController()); this.control.current_object = obj; this.control.using_type = DetermineUsageType(obj); this.control.alt = ctrl != CON_Use; if (this->~HasVirtualCursor()) { var cursor = this->~VirtualCursor(), angle; if (!cursor->IsActive() && (angle = obj->~DefaultCrosshairAngle(this, GetDir() * 2 - 1))) { x = +Sin(angle, CURSOR_Radius, 10); y = -Cos(angle, CURSOR_Radius, 10); } cursor->StartAim(this, angle); } var hold_enabled = obj->Call("~HoldingEnabled"); if (hold_enabled) SetPlayerControlEnabled(GetOwner(), CON_Aim, true); // first call UseStart. If unhandled, call Use (mousecontrol) var handled = obj->Call(GetUseCallString("Start"), this, x, y); if (!handled) { handled = obj->Call(GetUseCallString(), this, x, y); this.control.noholdingcallbacks = handled; } else { // *Start was handled. So clean up possible old noholdingcallbacks-values. this.control.noholdingcallbacks = false; } if (!handled) { this.control.current_object = nil; this.control.using_type = nil; if (hold_enabled) SetPlayerControlEnabled(GetOwner(), CON_Aim, false); return false; } else { this.control.started_use = true; // add helper effect that prevents errors when objects are suddenly deleted by quickly cancelling their use beforehand AddEffect("ItemRemovalCheck", GetUsedObject(), 1, 100, this, nil); // the slow timer is arbitrary and will just clean up the effect if necessary } return handled; } func CancelUseControl(int x, int y) { // forget possibly stored commands StopShelvedCommand(); // to horse first (if there is one) var horse = GetActionTarget(); if (horse && GetProcedure() == "ATTACH" && GetUsedObject() != horse) StopUseControl(x, y, horse, true); return StopUseControl(x, y, GetUsedObject(), true); } func StopUseControl(int x, int y, object obj, bool cancel) { var stop = "Stop"; if (cancel) stop = "Cancel"; // ControlUseStop, ControlUseAltStop, ContainedUseAltStop, ContainedUseCancel, etc... var handled = false; if (obj) { handled = obj->Call(GetUseCallString(stop), this, x, y); } if (obj == GetUsedObject()) { // if ControlUseStop returned -1, the current object is kept as "used object" // but no more callbacks except for ControlUseCancel are made. The usage of this // object is finally cancelled on ControlUseCancel. if (cancel || handled != -1) { // look for correct removal helper effect and remove it var effect_index = 0; var removal_helper = nil; do { removal_helper = GetEffect("ItemRemovalCheck", GetUsedObject(), effect_index++); if (!removal_helper) break; if (removal_helper.CommandTarget != this) continue; break; } while (true); RemoveEffect("IntControlFreeHands", this); // make sure we can climb again this.control.current_object = nil; this.control.using_type = nil; this.control.alt = false; if (removal_helper) { RemoveEffect(nil, nil, removal_helper, true); } } this.control.noholdingcallbacks = false; SetPlayerControlEnabled(GetOwner(), CON_Aim, false); if (this->~HasVirtualCursor()) this->~VirtualCursor()->StopAim(); } return handled; } func HoldingUseControl(int ctrl, int x, int y, object obj) { var mex = x; var mey = y; //Message("%d,%d",this,mex,mey); // automatic adjustment of the direction // -------------------- // if this is desired for ALL objects is the question, we will find out. // For now, this is done for items and vehicles, not for buildings and // mounts (naturally). If turning vehicles just like that without issuing // a turning animation is the question. But after all, the clonk can turn // by setting the dir too. // not riding and not in building not while scaling if (GetProcedure() != "ATTACH" && !Contained() && GetProcedure() != "SCALE") { // pushing vehicle: object to turn is the vehicle var dir_obj = GetActionTarget(); if (GetProcedure() != "PUSH") dir_obj = nil; // otherwise, turn the clonk if (!dir_obj) dir_obj = this; if ((dir_obj->GetComDir() == COMD_Stop && dir_obj->GetXDir() == 0) || dir_obj->GetProcedure() == "FLIGHT") { if (dir_obj->GetDir() == DIR_Left) { if (mex > 0) dir_obj->SetDir(DIR_Right); } else { if (mex < 0) dir_obj->SetDir(DIR_Left); } } } var handled = obj->Call(GetUseCallString("Holding"), this, mex, mey); return handled; } // very infrequent timer to prevent dangling effects, this is not necessary for correct functioning func FxItemRemovalCheckTimer(object target, proplist effect, int time) { if (!effect.CommandTarget) return -1; if (effect.CommandTarget->~GetUsedObject() != target) return -1; return 1; } // this will be called when an inventory object (that is in use) is suddenly removed func FxItemRemovalCheckStop(object target, proplist effect, int reason, bool temporary) { if (temporary) return; // only trigger when the object has been removed if (reason != FX_Call_RemoveClear) return; // safety if (!effect.CommandTarget) return; if (effect.CommandTarget->~GetUsedObject() != target) return; // quickly cancel use in a clean way while the object is still available effect.CommandTarget->CancelUse(); return; } // Control use redirected to script func ControlUse2Script(int ctrl, int x, int y, int strength, bool repeat, int status, object obj) { if (ctrl == CON_Use || ctrl == CON_UseAlt) { // cancel usage if a menu pops up if (this->~GetMenu()) { CancelUse(); return true; } // standard use if (status == CONS_Down && !repeat) { return StartUseControl(ctrl, x, y, obj); } else if (status == CONS_Up && (obj == GetUsedObject() || obj == GetActionTarget())) { return StopUseControl(x, y, obj); } // more use (holding) if (status == CONS_Up) { // leftover use release CancelUse(); return true; } else if (status == CONS_Down && repeat && !this.control.noholdingcallbacks) { return HoldingUseControl(ctrl, x, y, obj); } } return false; }