| 01-19-2009, 07:39 AM | #1 |
LastOrder Library Background: This library of code allows lots of cool interfacing with the last orders of a unit. The main point of the library is to maintain an organized list of up to N of any given unit's last issued orders. The value of this is that you can recall those orders at any time and reissue them if the need arises. (Such as if those orders were lost to the unit by a pause/stop/pause or something similar) Thanks to ToukoAozaki for giving me the motivation to package and release it. Requirements:
Library:library LastOrder initializer Init //****************************************************************************** //* BY: Rising_Dusk //* //* LastOrder is a library that was designed to allow interfacing with the last //* N orders any unit on your map has received. This library was also designed //* to be used as a means to reissue lost orders to a unit either after //* preventing a spell cast or in many other situations. //* //* There are two configuration constants for you to play with in using this //* script. ORDERS_TO_HOLD is basically the size of the game's memory for each //* individual unit. The larger the number is, the further back you can retrace //* a unit's order list. Setting this value to 3 suffices for all actual //* mechanics purposes. Raise it only if you have a specific application where //* you need more. Lowering it to 2 covers most cases, but may miss a few, and //* lowering it to 1 prevents you from adequately canceling spells and properly //* reissuing previous orders. I recommend leaving it alone at 3. The MAX_ORDERS //* constant is the number of orders that the system holds over all units at a //* given time. If you are worried about running out, go ahead and increase it, //* but be aware that it will use big arrays (which are slower) if you use a //* number over 8191. Don't lower it under 8191, there's no reason to. Don't //* change the non-private constants, there's no reason to. //* //* function GetPastOrder takes unit u, integer whichOrder returns order //* function GetPastOrderId takes unit u, integer whichOrder returns integer //* function GetPastOrderString takes unit u, integer whichOrder returns string //* function GetPastOrderType takes unit u, integer whichOrder returns integer //* function GetPastOrderX takes unit u, integer whichOrder returns real //* function GetPastOrderY takes unit u, integer whichOrder returns real //* function GetPastOrderTarget takes unit u, integer whichOrder returns widget //* //* The above API is the main list of functions the user can use to interface //* with past orders. If you want the immediate last order for a player, use 1 //* for the whichOrder integer. The 'order' returned by GetPastOrder is a struct //* that contains all information of an issued order. If you want to interface //* directly with the struct and skip all of the interfacing wrappers, you have //* access to them via the following commands: //* //* [unit] .u The unit being ordered. //* [integer] .id The order id of the past order. //* [integer] .typ The type of the past order. (See constants) //* [boolean] .fin A flag indicating whether the order was finished. //* [widget] .tar The target widget of the past order. //* [real] .x The x coordinate of the target point of the order. //* [real] .y The y coordinate of the target point of the order. //* //* There is also a sizable API for backwards compatibility with older versions //* of the library. This API assumes that you're talking about a specific past //* order, either the lastmost order or the second lastmost. In most cases, //* these are still useful because they remove an argument from the call. //* //* function GetLastOrder takes unit u returns order //* function GetLastOrderId takes unit u returns integer //* function GetLastOrderString takes unit u returns string //* function GetLastOrderType takes unit u returns integer //* function GetLastOrderX takes unit u returns real //* function GetLastOrderY takes unit u returns real //* function GetLastOrderTarget takes unit u returns widget //* function IsLastOrderFinished takes unit u returns boolean //* //* Besides being able to get information about all of the past orders a unit //* has been issued, the most useful part of this system is actually reissuing //* those orders as a means to fix lost orders or intercept and prevent spell //* casting. The following API is then available to the user: //* //* function IssuePastOrder takes unit u, integer whichOrder returns boolean //* function IssueLastOrder takes unit u returns boolean //* function IssueSecondLastOrder takes unit u returns boolean //* function AbortOrder takes unit u returns boolean //* //* If you want to reissue a past order for a given unit, IssuePastOrder is the //* function that you'll want to use on a unit. To issue the last or second last //* orders, there are functions for those as well. (They mostly exist for //* backwards compatibility) AbortOrder is a means for anyone using this script //* to stop a unit from running any given order that they happen to have at any //* moment. AbortOrder is used in conjunction with a supplementary library, //* AbortSpell, to seamlessly replicate WC3's spell error messages. //* //* function IssueArbitraryOrder takes unit u, order o returns boolean //* //* IssueArbitraryOrder is special in that it ignores many of the normal checks //* and automatically forces a unit to take on a specific order. This can be //* convenient if you want to make one unit take on another unit's order, for //* instance. Be forewarned that this overwrites whatever the unit was doing and //* issues the order no matter what, so use only as needed. //* //* If you have any further questions regarding LastOrder or how to use it, feel //* free to visit [url]www.wc3c.net[/url] and ask questions there. This library should only //* ever be released at WC3C and at no other site. Please give credits if this //* library finds its way into your maps, and otherwise thanks for reading! //* //* Enjoy! //* globals //Order type variables constant integer ORDER_TYPE_TARGET = 1 constant integer ORDER_TYPE_POINT = 2 constant integer ORDER_TYPE_IMMEDIATE = 3 //System constants private constant integer ORDERS_TO_HOLD = 3 //How many past orders the //system holds for each unit. //Should be at least 2. private constant integer MAX_ORDERS = 8191 //The max number of orders //the system can maintain. //Going over 8191 uses big //arrays, which are slower //than normal arrays. endglobals globals private hashtable ht = InitHashtable() endglobals struct order[MAX_ORDERS] unit u integer id integer typ boolean fin widget tar real x real y static method create takes unit ordered, integer ordid, integer ordtyp, widget target, real ordx, real ordy returns order local order o = order.allocate() local integer i = ORDERS_TO_HOLD local integer hid = GetHandleId(ordered) set o.u = ordered set o.id = ordid set o.typ = ordtyp set o.fin = false set o.tar = target set o.x = ordx set o.y = ordy //Handle stored orders in the hashtable loop //We hold up to the constant ORDERS_TO_HOLD in the table exitwhen i == 1 //Moves the N-1th order to the Nth slot, etc. except storing new last order if HaveSavedInteger(ht, hid, i-1) then if i == ORDERS_TO_HOLD and HaveSavedInteger(ht, hid, i) then //Destroy lastmost order struct call order.destroy(order(LoadInteger(ht, hid, i))) endif //Can only do this if the N-1th order exists call SaveInteger(ht, hid, i, LoadInteger(ht, hid, i-1)) endif set i = i - 1 endloop //Store the new order to the hashtable as 1th last order call SaveInteger(ht, hid, 1, integer(o)) return o endmethod endstruct //****************************************************************************** //! textmacro LastOrderDebug takes ORDER, RETURN debug if $ORDER$ > ORDERS_TO_HOLD then debug call BJDebugMsg(SCOPE_PREFIX+"Error: Order out of range") debug return $RETURN$ debug endif debug if not HaveSavedInteger(ht, GetHandleId(u), $ORDER$) then debug call BJDebugMsg(SCOPE_PREFIX+"Error: The "+I2S($ORDER$)+"th order doesn't exist") debug return $RETURN$ debug endif //! endtextmacro function GetPastOrder takes unit u, integer whichOrder returns order //! runtextmacro LastOrderDebug("whichOrder", "0") return order(LoadInteger(ht, GetHandleId(u), whichOrder)) endfunction function GetPastOrderId takes unit u, integer whichOrder returns integer //! runtextmacro LastOrderDebug("whichOrder", "0") return order(LoadInteger(ht, GetHandleId(u), whichOrder)).id endfunction function GetPastOrderString takes unit u, integer whichOrder returns string //! runtextmacro LastOrderDebug("whichOrder", "\"\"") return OrderId2String(order(LoadInteger(ht, GetHandleId(u), whichOrder)).id) endfunction function GetPastOrderType takes unit u, integer whichOrder returns integer //! runtextmacro LastOrderDebug("whichOrder", "0") return order(LoadInteger(ht, GetHandleId(u), whichOrder)).typ endfunction function GetPastOrderX takes unit u, integer whichOrder returns real //! runtextmacro LastOrderDebug("whichOrder", "0.") return order(LoadInteger(ht, GetHandleId(u), whichOrder)).x endfunction function GetPastOrderY takes unit u, integer whichOrder returns real //! runtextmacro LastOrderDebug("whichOrder", "0.") return order(LoadInteger(ht, GetHandleId(u), whichOrder)).y endfunction function GetPastOrderTarget takes unit u, integer whichOrder returns widget //! runtextmacro LastOrderDebug("whichOrder", "null") return order(LoadInteger(ht, GetHandleId(u), whichOrder)).tar endfunction //****************************************************************************** function GetLastOrder takes unit u returns order return GetPastOrder(u, 1) endfunction function GetLastOrderId takes unit u returns integer return GetPastOrderId(u, 1) endfunction function GetLastOrderString takes unit u returns string return GetPastOrderString(u, 1) endfunction function GetLastOrderType takes unit u returns integer return GetPastOrderType(u, 1) endfunction function GetLastOrderX takes unit u returns real return GetPastOrderX(u, 1) endfunction function GetLastOrderY takes unit u returns real return GetPastOrderY(u, 1) endfunction function GetLastOrderTarget takes unit u returns widget return GetPastOrderTarget(u, 1) endfunction function IsLastOrderFinished takes unit u returns boolean //! runtextmacro LastOrderDebug("1", "false") return GetUnitCurrentOrder(u) == 0 or order(LoadInteger(ht, GetHandleId(u), 1)).fin endfunction //****************************************************************************** private function OrderFilter takes unit u, integer id returns boolean //* Excludes specific orders or unit types from registering with the system //* //* 851972: stop //* Stop is excluded from the system because it is the order that //* tells a unit to do nothing. It should be ignored by the system. //* //* 851971: smart //* 851986: move //* 851983: attack //* 851984: attackground //* 851990: patrol //* 851993: holdposition //* These are the UI orders that are passed to the system. //* //* 851973: stunned //* This order is issued when a unit is stunned onto the stunner //* It's ignored by the system, since you'd never want to reissue it //* //* >= 852055, <= 852762 //* These are all spell IDs from defend to incineratearrowoff with //* a bit of leeway at the ends for orders with no strings. //* return id == 851971 or id == 851986 or id == 851983 or id == 851984 or id == 851990 or id == 851993 or (id >= 852055 and id <= 852762) endfunction private function IssuePastOrderFilter takes unit u, integer whichOrder returns boolean //* Some criteria for whether or not a unit's last order should be given //* //* INSTANT type orders are excluded because generally, reissuing an instant //* order doesn't make sense. You can remove that check below if you'd like, //* though. //* //* The Type check is really just to ensure that no spell recursion can //* occur with IssueLastOrder. The problem with intercepting the spell cast //* event is that it happens after the order is 'caught' and registered to //* this system. Therefore, to just IssueLastOrder tells it to recast the //* spell! That's a problem, so we need a method to eliminate it. //* return GetUnitTypeId(u) != 0 and not IsUnitType(u, UNIT_TYPE_DEAD) and GetPastOrderType(u, whichOrder) != 0 and GetPastOrderType(u, whichOrder) != ORDER_TYPE_IMMEDIATE endfunction //****************************************************************************** function IssuePastOrder takes unit u, integer whichOrder returns boolean local order o = GetPastOrder(u, whichOrder) if IssuePastOrderFilter(u, whichOrder) and not o.fin then if o.typ == ORDER_TYPE_TARGET then return IssueTargetOrderById(u, o.id, o.tar) elseif o.typ == ORDER_TYPE_POINT then if o.id == 851971 then //This adjusts for a bug in the point order's boolean return //when issuing a smart order call IssuePointOrderById(u, o.id, o.x, o.y) return true else return IssuePointOrderById(u, o.id, o.x, o.y) endif elseif o.typ == ORDER_TYPE_IMMEDIATE then return IssueImmediateOrderById(u, o.id) endif endif return false endfunction function IssueLastOrder takes unit u returns boolean return IssuePastOrder(u, 1) endfunction function IssueSecondLastOrder takes unit u returns boolean return IssuePastOrder(u, 2) endfunction function IssueArbitraryOrder takes unit u, order o returns boolean if o.typ == ORDER_TYPE_TARGET then return IssueTargetOrderById(u, o.id, o.tar) elseif o.typ == ORDER_TYPE_POINT then if o.id == 851971 then //This adjusts for a bug in the point order's boolean return //when issuing a smart order call IssuePointOrderById(u, o.id, o.x, o.y) return true else return IssuePointOrderById(u, o.id, o.x, o.y) endif elseif o.typ == ORDER_TYPE_IMMEDIATE then return IssueImmediateOrderById(u, o.id) endif return false endfunction function AbortOrder takes unit u returns boolean if IsUnitPaused(u) then return false else call PauseUnit(u, true) call IssueImmediateOrder(u, "stop") call PauseUnit(u, false) endif return true endfunction //********************************************************** private function Conditions takes nothing returns boolean return OrderFilter(GetTriggerUnit(), GetIssuedOrderId()) endfunction private function Actions takes nothing returns nothing local unit u = GetTriggerUnit() local widget t = GetOrderTarget() local integer oid = GetIssuedOrderId() local integer oty = 0 if GetTriggerEventId() == EVENT_PLAYER_UNIT_ISSUED_TARGET_ORDER then call order.create(u, oid, ORDER_TYPE_TARGET, t, GetWidgetX(t), GetWidgetY(t)) elseif GetTriggerEventId() == EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER then call order.create(u, oid, ORDER_TYPE_POINT, null, GetOrderPointX(), GetOrderPointY()) elseif GetTriggerEventId() == EVENT_PLAYER_UNIT_ISSUED_ORDER then call order.create(u, oid, ORDER_TYPE_IMMEDIATE, null, GetUnitX(u), GetUnitY(u)) debug else debug call BJDebugMsg(SCOPE_PREFIX+"Error: Invalid order type") endif set u = null set t = null endfunction //********************************************************** private function SpellActions takes nothing returns nothing local order o = GetPastOrder(GetTriggerUnit(), 1) set o.fin = true endfunction //********************************************************** private function OnAdd takes nothing returns boolean local integer hid = GetHandleId(GetFilterUnit()) local integer i = ORDERS_TO_HOLD //Handle stored orders in the hashtable loop //We hold up to the constant ORDERS_TO_HOLD in the table exitwhen i == 0 //If any of the N orders exist for this handle id, kill them all if HaveSavedInteger(ht, hid, i) then call order.destroy(order(LoadInteger(ht, hid, i))) call RemoveSavedInteger(ht, hid, i) endif set i = i - 1 endloop return false endfunction //********************************************************** private function Init takes nothing returns nothing local trigger trg = CreateTrigger() local region re = CreateRegion() local rect m = GetWorldBounds() //Main order catching trigger call TriggerAddAction(trg, function Actions) call TriggerAddCondition(trg, Condition(function Conditions)) call TriggerRegisterAnyUnitEventBJ(trg, EVENT_PLAYER_UNIT_ISSUED_TARGET_ORDER) call TriggerRegisterAnyUnitEventBJ(trg, EVENT_PLAYER_UNIT_ISSUED_POINT_ORDER) call TriggerRegisterAnyUnitEventBJ(trg, EVENT_PLAYER_UNIT_ISSUED_ORDER) //Spell trigger to set a flag that indicates a spell order's completion set trg = CreateTrigger() call TriggerAddAction(trg, function SpellActions) call TriggerRegisterAnyUnitEventBJ(trg, EVENT_PLAYER_UNIT_SPELL_EFFECT) //Entering world trigger that clears old data from handle ids set trg = CreateTrigger() call RegionAddRect(re, m) call TriggerRegisterEnterRegion(trg, re, Condition(function OnAdd)) call RemoveRect(m) set trg = null set re = null set m = null endfunction endlibrary Function List: This library gives the following interfacing options with last orders. It contains the following functions for use with interfacing last orders given to a unit.
Attached to this post is a demo map showing you how the system works. You can hit escape to view the last order details for your blademaster at any given time. Additionally, you can issue orders around the map and then use the "Demo Spell" with the cancel icon. That spell does nothing, but it is intercepted by an example code in the demo map and takes advantage of the following AbortSpell library, which was designed to be used with the LastOrder library. This should be a sufficient example for anyone to figure out how to use it in their maps to its fullest extent. _______________________________________________________________________
AbortSpell Supplementary Library Requirements: Supplementary Library:library AbortSpell requires LastOrder, SimError //****************************************************************************** //* BY: Rising_Dusk //* //* The AbortSpell function in this library works just like the normal //* SimError, except that it gives options for reissuing the unit's last order //* and forcing a UI key for its owner to fully simulate a WC3 error message as //* close as humanly possible. //* //* AbortSpell is valuable when used in the ISSUED_ORDER trigger event //* callbacks. It is specifically designed to be used as a means for preventing //* a unit from casting a spell and still continuing with whatever their last //* order was. It also works on the SPELL_CAST event callback, but using it on //* the order callbacks prevents the caster from turning/walking towards the //* target point/unit/etc. //* //* Sample Function Usage: //* call AbortSpell(MyUnit, "Error Message", "G") //* function AbortSpell takes unit u, string msg, string key returns boolean local boolean b = false local real a = 0. if AbortOrder(u) then call SimError(GetOwningPlayer(u), "\n"+msg) if GetLocalPlayer() == GetOwningPlayer(u) and StringLength(key) == 1 and key != " " then call ForceUIKey(key) endif set b = IssueSecondLastOrder(u) if not b and GetPastOrder(u, 2) != 0 then //Failed, have to issue a dummy order to prevent spell recursion //Have to project a point a fraction of a space ahead of the unit, //otherwise it will turn to 0 degrees facing because Blizzard sucks set a = GetUnitFacing(u)*bj_DEGTORAD call IssuePointOrderById(u, 851971, GetUnitX(u)+0.01*Cos(a), GetUnitY(u)+0.01*Sin(a)) endif endif return b endfunction endlibrary Function List: This library gives the following function to the user.
Thanks guys, comments are welcome! |
| 01-19-2009, 10:17 AM | #2 |
That a really greet idea! But on my pc (wc3 1.21b) it tells me about hit op limit in the init function. For me it works without the initialization loop exactly the same like before. +Rep |
| 01-19-2009, 01:06 PM | #3 |
I found two problems using the cancel spell in your test map. First was that if you cast Mirror Image and then do nothing until its cooldown wears off and then use the cancel spell, the hero recasts Mirror Image. Second was that shift orders are forgotten when the error spell is used. Also, I would name those ORDER_TYPE_ constants according to the Issue(Target|Point|Immediate)Order functions that they are associated with. |
| 01-19-2009, 01:17 PM | #4 |
I think SimErrorEx should be separated in another module, mostly because I think that the current method is not the best possible, but the alternative requires more code, pausing units is a little problematic, often I prefer to give the correct order after a 0.0 seconds timer rather than pause the unit. edit: Making SimErrorEx a separate thing will also allow the main library to get rid of the SimError requirement. |
| 01-19-2009, 01:29 PM | #5 |
Fuck yes. You rule Dusk. I've been meaning to do something like this for a long time, and now I don't have to. Pro. |
| 01-19-2009, 04:38 PM | #6 | |||
Quote:
EDIT: Actually, thanks for the report. It seems I was checking Order and not P_Order, the second to last order. Fixing that bug now. Quote:
Let me split the library up a bit, I'll update shortly. Quote:
|
| 01-19-2009, 05:42 PM | #7 |
Is it really required to initialize all those array values? Aren't uninitialized arrays effectively the same as initial values of 0/null as far as comparisons go? While not a requirement, it is my suggestion that you make an additional "flavour" of this library using table; in any case this can wait until the end of the review process when the indexing version of the library will be finalised. |
| 01-19-2009, 06:27 PM | #8 |
This is the sort of thing in which I wouldn't use Table, It would at least need a unit group trick to be safe. |
| 01-19-2009, 07:47 PM | #9 |
Okay, I split them up and fixed all of the documentation. I also removed those array initializations, since Ani cleared up that one for me. |
| 01-21-2009, 02:17 PM | #10 |
I think it is a good thing. Though I suggest you to make a UnitIndexingutilities that doesn't use user data so that people are not disallowed to use it if user data is used by something else. hmnn. |
| 01-21-2009, 02:34 PM | #11 | |
Quote:
|
| 01-21-2009, 02:50 PM | #12 |
It kind of becomes an issue when there are thousands of different indexing systems. |
| 01-21-2009, 03:11 PM | #13 |
That's certainly true. I wish we could streamline the use of userdata (like with UnitIndexingUtils) like you did timers (with TimerUtils) and gamecache (with Table); it would remove the fuzziness in that sort of thing. |
| 02-11-2009, 08:19 AM | #14 |
Just to remind you from my PM. Is it possible that you can create a "flavor" which uses Table instead of your own system Unit Indexing Utilities. I reckon Table has a higher (I don't think it is the right term oh well) re-use-ability rate. So I highly suggest that you do. -Av3n |
| 02-11-2009, 11:50 AM | #15 |
I thought about this. The problem with having two versions requiring different libraries is that then it becomes absolutely critical which version of the library users are using. It may as well be like having two entirely different libraries, since they are not interchangeable with each other at all. I'll ask Vex/Ani about it and then go from there, more than likely. |
