@@ -106,6 +106,8 @@ local GROUP_ROLL_MAX_ITEM_LINK_LENGTH = 160
106106local STATE_TIMEOUT_SECONDS = 5.0
107107local STATES_TIMEOUT_SECONDS = 15.0
108108local STRATEGY_MUTATION_TIMEOUT_SECONDS = 5.0
109+ Comm ._GROUP_ORDER_TIMEOUT_SECONDS = 5.0
110+ Comm ._GROUP_ORDER_MAX_ACTIVE = 8
109111local SELF_STRATEGY_MUTATION_TIMEOUT_SECONDS = 10.0
110112local SELF_ACTION_TIMEOUT_SECONDS = 10.0
111113local STRATEGY_MUTATION_MAX_ACTIVE = 32
@@ -1850,6 +1852,156 @@ local function validateStrategyMutationChanges(changes)
18501852 return table.concat (normalized , " ," )
18511853end
18521854
1855+ -- MB_FOLLOW_STAY_ORDER_V1_TX_BEGIN
1856+ function Comm ._FinishGroupOrderCommand (token , result )
1857+ local state = ensureBridgeState ()
1858+ state .groupOrderCommands = state .groupOrderCommands or {}
1859+
1860+ local pending = state .groupOrderCommands [token ]
1861+ if type (pending ) ~= " table" then
1862+ return false
1863+ end
1864+
1865+ state .groupOrderCommands [token ] = nil
1866+ result = type (result ) == " table" and result or {}
1867+ result .token = token
1868+ result .order = result .order or pending .order
1869+
1870+ if type (pending .callback ) == " function" then
1871+ pending .callback (result )
1872+ end
1873+
1874+ if MultiBot .OnGroupOrderApplied then
1875+ MultiBot .OnGroupOrderApplied (result .order , result )
1876+ end
1877+
1878+ return true
1879+ end
1880+
1881+ function Comm .RunGroupOrderCommand (order , callback )
1882+ local state = ensureBridgeState ()
1883+ if not state .connected then
1884+ state .lastError = " GROUP_ORDER_NOT_CONNECTED"
1885+ return false
1886+ end
1887+
1888+ order = string.upper (trim (order or " " ))
1889+ if order ~= " FOLLOW" and order ~= " STAY" then
1890+ state .lastError = " GROUP_ORDER_INVALID"
1891+ return false
1892+ end
1893+
1894+ state .groupOrderCommands = state .groupOrderCommands or {}
1895+ local active = 0
1896+ for _ in pairs (state .groupOrderCommands ) do
1897+ active = active + 1
1898+ end
1899+ if active >= Comm ._GROUP_ORDER_MAX_ACTIVE then
1900+ state .lastError = " GROUP_ORDER_BUSY"
1901+ return false
1902+ end
1903+
1904+ state .groupOrderSeq = (tonumber (state .groupOrderSeq ) or 0 ) + 1
1905+ local token = tostring (math.floor (safeNow () * 1000 )) .. " -group-order-" .. tostring (state .groupOrderSeq )
1906+ state .groupOrderCommands [token ] = {
1907+ order = order ,
1908+ callback = type (callback ) == " function" and callback or nil ,
1909+ startedAt = safeNow (),
1910+ }
1911+
1912+ if not Comm .Send (" RUN" , order .. " _ORDER~" .. token ) then
1913+ state .groupOrderCommands [token ] = nil
1914+ state .lastError = " GROUP_ORDER_SEND_FAILED"
1915+ return false
1916+ end
1917+
1918+ safeDelay (Comm ._GROUP_ORDER_TIMEOUT_SECONDS , function ()
1919+ local bridge = ensureBridgeState ()
1920+ bridge .groupOrderCommands = bridge .groupOrderCommands or {}
1921+ if not bridge .groupOrderCommands [token ] then
1922+ return
1923+ end
1924+
1925+ bridge .lastError = " GROUP_ORDER_TIMEOUT~" .. token
1926+ Comm ._FinishGroupOrderCommand (token , {
1927+ status = " timeout" ,
1928+ matched = 0 ,
1929+ succeeded = 0 ,
1930+ failed = 0 ,
1931+ reason = " TIMEOUT" ,
1932+ })
1933+ end )
1934+
1935+ return token
1936+ end
1937+ -- MB_FOLLOW_STAY_ORDER_V1_TX_END
1938+
1939+ -- MB_ATTACK_ORDER_V1_TX_BEGIN
1940+ function Comm .RunAttackOrderCommand (audience , callback )
1941+ local state = ensureBridgeState ()
1942+ if not state .connected then
1943+ state .lastError = " ATTACK_ORDER_NOT_CONNECTED"
1944+ return false
1945+ end
1946+
1947+ audience = string.upper (trim (audience or " " ))
1948+ if audience ~= " ALL"
1949+ and audience ~= " TANK"
1950+ and audience ~= " HEALER"
1951+ and audience ~= " DPS"
1952+ and audience ~= " MELEE"
1953+ and audience ~= " RANGED" then
1954+ state .lastError = " ATTACK_ORDER_BAD_AUDIENCE"
1955+ return false
1956+ end
1957+
1958+ state .groupOrderCommands = state .groupOrderCommands or {}
1959+ local active = 0
1960+ for _ in pairs (state .groupOrderCommands ) do
1961+ active = active + 1
1962+ end
1963+ if active >= Comm ._GROUP_ORDER_MAX_ACTIVE then
1964+ state .lastError = " ATTACK_ORDER_BUSY"
1965+ return false
1966+ end
1967+
1968+ state .groupOrderSeq = (tonumber (state .groupOrderSeq ) or 0 ) + 1
1969+ local token = tostring (math.floor (safeNow () * 1000 )) .. " -attack-order-" .. tostring (state .groupOrderSeq )
1970+ state .groupOrderCommands [token ] = {
1971+ order = " ATTACK" ,
1972+ audience = audience ,
1973+ callback = type (callback ) == " function" and callback or nil ,
1974+ startedAt = safeNow (),
1975+ }
1976+
1977+ if not Comm .Send (" RUN" , " ATTACK_ORDER~" .. token .. " ~" .. audience ) then
1978+ state .groupOrderCommands [token ] = nil
1979+ state .lastError = " ATTACK_ORDER_SEND_FAILED"
1980+ return false
1981+ end
1982+
1983+ safeDelay (Comm ._GROUP_ORDER_TIMEOUT_SECONDS , function ()
1984+ local bridge = ensureBridgeState ()
1985+ bridge .groupOrderCommands = bridge .groupOrderCommands or {}
1986+ if not bridge .groupOrderCommands [token ] then
1987+ return
1988+ end
1989+
1990+ bridge .lastError = " ATTACK_ORDER_TIMEOUT~" .. token
1991+ Comm ._FinishGroupOrderCommand (token , {
1992+ status = " timeout" ,
1993+ audience = audience ,
1994+ matched = 0 ,
1995+ succeeded = 0 ,
1996+ failed = 0 ,
1997+ reason = " TIMEOUT" ,
1998+ })
1999+ end )
2000+
2001+ return token
2002+ end
2003+ -- MB_ATTACK_ORDER_V1_TX_END
2004+
18532005local function finishStrategyMutationCommand (token , result )
18542006 local state = ensureBridgeState ()
18552007 local pending = state .strategyMutationCommands [token ]
@@ -9764,6 +9916,146 @@ function Comm.HandleAddonMessage(prefix, message, distribution, sender)
97649916 return true
97659917 end
97669918
9919+ -- MB_FOLLOW_STAY_ORDER_V1_RX_BEGIN
9920+ if opcode == " FOLLOW_ORDER_ACK" or opcode == " STAY_ORDER_ACK" then
9921+ local fields = splitFields (payload or " " )
9922+ if # fields ~= 5 then
9923+ state .lastError = " GROUP_ORDER_ACK_BAD_FIELD_COUNT"
9924+ return true
9925+ end
9926+
9927+ local token = fields [1 ]
9928+ local matched = tonumber (fields [2 ])
9929+ local succeeded = tonumber (fields [3 ])
9930+ local failed = tonumber (fields [4 ])
9931+ local reason = fields [5 ]
9932+ local order = opcode == " FOLLOW_ORDER_ACK" and " FOLLOW" or " STAY"
9933+ state .groupOrderCommands = state .groupOrderCommands or {}
9934+ local pending = state .groupOrderCommands [token ]
9935+
9936+ local countsValid = matched and succeeded and failed
9937+ and matched >= 0 and matched <= 40 and math.floor (matched ) == matched
9938+ and succeeded >= 0 and succeeded <= matched and math.floor (succeeded ) == succeeded
9939+ and failed >= 0 and failed <= matched and math.floor (failed ) == failed
9940+ and succeeded + failed == matched
9941+
9942+ if not isValidStateToken (token )
9943+ or not countsValid
9944+ or type (reason ) ~= " string"
9945+ or reason == " "
9946+ or string.len (reason ) > 32
9947+ or not string.match (reason , " ^[A-Z0-9_]+$" )
9948+ or type (pending ) ~= " table"
9949+ or pending .order ~= order then
9950+ state .lastError = " GROUP_ORDER_ACK_INVALID"
9951+ return true
9952+ end
9953+
9954+ state .connected = true
9955+ state .lastError = reason == " OK" and nil or (" GROUP_ORDER_" .. reason )
9956+ debugPrint (" ADDON:RX" , opcode , payload or " " )
9957+
9958+ local status = " failed"
9959+ if matched == 0 then
9960+ status = " empty"
9961+ elseif failed == 0 and succeeded == matched then
9962+ status = " ok"
9963+ elseif succeeded > 0 then
9964+ status = " partial"
9965+ end
9966+
9967+ Comm ._FinishGroupOrderCommand (token , {
9968+ status = status ,
9969+ matched = matched ,
9970+ succeeded = succeeded ,
9971+ failed = failed ,
9972+ reason = reason ,
9973+ })
9974+ return true
9975+ end
9976+ -- MB_FOLLOW_STAY_ORDER_V1_RX_END
9977+
9978+ -- MB_ATTACK_ORDER_V1_RX_BEGIN
9979+ if opcode == " ATTACK_ORDER_ACK" then
9980+ local fields = splitFields (payload or " " )
9981+ if # fields ~= 6 then
9982+ state .lastError = " ATTACK_ORDER_ACK_BAD_FIELD_COUNT"
9983+ return true
9984+ end
9985+
9986+ local token = fields [1 ]
9987+ local audience = fields [2 ]
9988+ local matched = tonumber (fields [3 ])
9989+ local succeeded = tonumber (fields [4 ])
9990+ local failed = tonumber (fields [5 ])
9991+ local reason = fields [6 ]
9992+ state .groupOrderCommands = state .groupOrderCommands or {}
9993+ local pending = state .groupOrderCommands [token ]
9994+
9995+ local audienceValid = audience == " ALL"
9996+ or audience == " TANK"
9997+ or audience == " HEALER"
9998+ or audience == " DPS"
9999+ or audience == " MELEE"
10000+ or audience == " RANGED"
10001+
10002+ local countsValid = matched and succeeded and failed
10003+ and matched >= 0 and matched <= 40 and math.floor (matched ) == matched
10004+ and succeeded >= 0 and succeeded <= matched and math.floor (succeeded ) == succeeded
10005+ and failed >= 0 and failed <= matched and math.floor (failed ) == failed
10006+ and succeeded + failed == matched
10007+
10008+ local reasonValid = reason == " OK"
10009+ or reason == " BAD_AUDIENCE"
10010+ or reason == " RATE_LIMIT"
10011+ or reason == " REPLAY"
10012+ or reason == " NO_GROUP"
10013+ or reason == " NO_TARGET"
10014+ or reason == " NO_BOTS"
10015+ or reason == " BOT_LIMIT"
10016+ or reason == " PARTIAL"
10017+ or reason == " FAILED"
10018+
10019+ if not isValidStateToken (token )
10020+ or not audienceValid
10021+ or not countsValid
10022+ or not reasonValid
10023+ or type (pending ) ~= " table"
10024+ or pending .order ~= " ATTACK"
10025+ or pending .audience ~= audience then
10026+ state .lastError = " ATTACK_ORDER_ACK_INVALID"
10027+ return true
10028+ end
10029+
10030+ state .connected = true
10031+ if reason == " OK" then
10032+ state .lastError = nil
10033+ else
10034+ state .lastError = " ATTACK_ORDER_" .. reason
10035+ end
10036+ debugPrint (" ADDON:RX" , opcode , payload or " " )
10037+
10038+ local status = " failed"
10039+ if reason == " OK" and matched > 0 and failed == 0 and succeeded == matched then
10040+ status = " ok"
10041+ elseif reason == " NO_BOTS" and matched == 0 then
10042+ status = " empty"
10043+ elseif succeeded > 0 then
10044+ status = " partial"
10045+ end
10046+
10047+ Comm ._FinishGroupOrderCommand (token , {
10048+ status = status ,
10049+ audience = audience ,
10050+ matched = matched ,
10051+ succeeded = succeeded ,
10052+ failed = failed ,
10053+ reason = reason ,
10054+ })
10055+ return true
10056+ end
10057+ -- MB_ATTACK_ORDER_V1_RX_END
10058+
976710059 if opcode == " RTI_ACK" then
976810060 state .connected = true
976910061 state .lastError = nil
0 commit comments