| 02-12-2009, 12:48 AM | #1 |
Suppose you want to run a league for your map. How do you do it? There are currently no standard tools to do this, and a major reason is the fact that it is so difficult to look at a replay file and even figure out who won. This system allows your map to place readable data in replays. You can output who won, who killed who, etc. It also does its best to deal with potential security issues like hackers trying to inject false data. The general goal is to make it possible to have a tool which can run leagues for any implementing map. If you output the information, they will come. The format used is called MMD v1.0. The popular GHost hosting bot supports this standard. Known Issues - The game cache sync actions (used by MMD) can cause TriggerSleepAction to terminate early. This can't be fixed except by avoiding straight TriggerSleepAction calls. TriggerSleepAction already has a bunch of known problems [affected by game speed, lagging, pausing ...] so you should already be using PolledWait or timers instead. [GUI: use Wait (Game-Time) instead of Wait]. JASS:/// [DESCRIPTION AND OTHER COMMENTS OMITTED DUE TO POST CHARACTER LIMIT] library MMD initializer init globals public constant integer GOAL_NONE = 101 public constant integer GOAL_HIGH = 102 public constant integer GOAL_LOW = 103 public constant integer TYPE_STRING = 101 public constant integer TYPE_REAL = 102 public constant integer TYPE_INT = 103 public constant integer OP_ADD = 101 public constant integer OP_SUB = 102 public constant integer OP_SET = 103 public constant integer SUGGEST_NONE = 101 public constant integer SUGGEST_TRACK = 102 public constant integer SUGGEST_LEADERBOARD = 103 public constant integer FLAG_DRAWER = 101 public constant integer FLAG_LOSER = 102 public constant integer FLAG_WINNER = 103 public constant integer FLAG_LEAVER = 104 public constant integer FLAG_PRACTICING = 105 endglobals globals private constant boolean SHOW_DEBUG_MESSAGES = true private constant string chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_-+= \\!@#$%^&*()/?>.<,;:'\"{}[]|`~" private constant integer num_chars = StringLength(chars) private string array flags private string array goals private string array ops private string array types private string array suggestions private boolean initialized = false private gamecache gc = null private constant string ESCAPED_CHARS = " \\" private constant integer CURRENT_VERSION = 1 private constant integer MINIMUM_PARSER_VERSION = 1 private constant string FILENAME = "MMD.Dat" private constant string M_KEY_VAL = "val:" private constant string M_KEY_CHK = "chk:" private constant integer NUM_SENDERS_NAIVE = 1 private constant integer NUM_SENDERS_SAFE = 3 private integer num_senders = NUM_SENDERS_NAIVE private integer num_msg = 0 private timer clock = CreateTimer() private string array q_msg private real array q_time private integer array q_index private keyword QueueNode private QueueNode q_head = 0 private QueueNode q_tail = 0 endglobals ///Triggered when tampering is detected. Increases the number of safeguards against tampering. public function RaiseGuard takes string reason returns nothing debug if SHOW_DEBUG_MESSAGES then debug call BJDebugMsg("MMD: Guard Raised! (" + reason + ")") debug endif set num_senders = NUM_SENDERS_SAFE //increase number of players voting on each message endfunction ///Returns seconds elapsed in game time private function time takes nothing returns real return TimerGetElapsed(clock) endfunction ///Initializes the char-to-int conversion private function prepC2I takes nothing returns nothing local integer i = 0 local string id loop exitwhen i >= num_chars set id = SubString(chars, i, i+1) if id == StringCase(id, true) then set id = id + "U" endif call StoreInteger(gc, "c2i", id, i) set i = i + 1 endloop endfunction ///Converts a character to an integer private function C2I takes string c returns integer local integer i local string id = c if id == StringCase(id, true) then set id = id + "U" endif set i = GetStoredInteger(gc, "c2i", id) if (i < 0 or i >= num_chars or SubString(chars, i, i+1) != c) and HaveStoredInteger(gc, "c2i", id) then //A cheater sent a fake sync to screw with the cached values set i = 0 loop exitwhen i >= num_chars //just a weird character if c == SubString(chars, i, i+1) then //cheating! call RaiseGuard("c2i poisoned") call StoreInteger(gc, "c2i", id, i) exitwhen true endif set i = i + 1 endloop endif return i endfunction ///Computes a weak hash value, hopefully secure enough for our purposes private function poor_hash takes string s, integer seed returns integer local integer n = StringLength(s) local integer m = n + seed local integer i = 0 loop exitwhen i >= n set m = m * 41 + C2I(SubString(s, i, i+1)) set i = i + 1 endloop return m endfunction ///Stores previously sent messages for tamper detection purposes private struct QueueNode readonly real timeout readonly string msg readonly integer checksum readonly string key public QueueNode next = 0 public static method create takes integer id, string msg returns QueueNode local QueueNode this = QueueNode.allocate() set .timeout = time() + 7.0 + GetRandomReal(0, 2+0.1*GetPlayerId(GetLocalPlayer())) set .msg = msg set .checksum = poor_hash(.msg, id) set .key = I2S(id) return this endmethod private method onDestroy takes nothing returns nothing call FlushStoredInteger(gc, M_KEY_VAL+.key, .msg) call FlushStoredInteger(gc, M_KEY_CHK+.key, .key) set .msg = null set .key = null set .next = 0 endmethod public method send takes nothing returns nothing call StoreInteger(gc, M_KEY_VAL+.key, .msg, .checksum) call StoreInteger(gc, M_KEY_CHK+.key, .key, .checksum) call SyncStoredInteger(gc, M_KEY_VAL+.key, .msg) call SyncStoredInteger(gc, M_KEY_CHK+.key, .key) endmethod endstruct ///Returns true for a fixed size uniform random subset of players in the game private function isEmitter takes nothing returns boolean local integer i = 0 local integer n = 0 local integer r local integer array picks local boolean array pick_flags loop exitwhen i >= 12 if GetPlayerController(Player(i)) == MAP_CONTROL_USER and GetPlayerSlotState(Player(i)) == PLAYER_SLOT_STATE_PLAYING then if n < num_senders then //initializing picks set picks[n] = i set pick_flags[i] = true else //maintain the invariant 'P(being picked) = c/n' set r = GetRandomInt(0, n) if r < num_senders then set pick_flags[picks[r]] = false set picks[r] = i set pick_flags[i] = true endif endif set n = n + 1 endif set i = i + 1 endloop return pick_flags[GetPlayerId(GetLocalPlayer())] endfunction ///Places meta-data in the replay and in network traffic private function emit takes string message returns nothing local QueueNode q if not initialized then call BJDebugMsg("MMD Emit Error: Library not initialized yet.") return endif //remember sent messages for tamper check set q = QueueNode.create(num_msg, message) if q_head == 0 then set q_head = q else set q_tail.next = q endif set q_tail = q //send new message set num_msg = num_msg + 1 if isEmitter() then call q.send() endif endfunction ///Performs tamper checks private function tick takes nothing returns nothing local QueueNode q local integer i //check previously sent messages for tampering set q = q_head loop exitwhen q == 0 or q.timeout >= time() if not HaveStoredInteger(gc, M_KEY_VAL+q.key, q.msg) then call RaiseGuard("message skipping") call q.send() elseif not HaveStoredInteger(gc, M_KEY_CHK+q.key, q.key) then call RaiseGuard("checksum skipping") call q.send() elseif GetStoredInteger(gc, M_KEY_VAL+q.key, q.msg) != q.checksum then call RaiseGuard("message tampering") call q.send() elseif GetStoredInteger(gc, M_KEY_CHK+q.key, q.key) != q.checksum then call RaiseGuard("checksum tampering") call q.send() endif set q_head = q.next call q.destroy() set q = q_head endloop if q_head == 0 then set q_tail = 0 endif //check for future message tampering set i = 0 loop exitwhen not HaveStoredInteger(gc, M_KEY_CHK+I2S(num_msg), I2S(num_msg)) call RaiseGuard("message insertion") call emit("Blank") set i = i + 1 exitwhen i >= 10 endloop endfunction ///Replaces control characters with escape sequences private function pack takes string value returns string local integer j local integer i = 0 local string result = "" local string c loop //for each character in argument string exitwhen i >= StringLength(value) set c = SubString(value, i, i+1) set j = 0 loop //for each character in escaped chars string exitwhen j >= StringLength(ESCAPED_CHARS) //escape control characters if c == SubString(ESCAPED_CHARS, j, j+1) then set c = "\\" + c exitwhen true endif set j = j + 1 endloop set result = result + c set i = i + 1 endloop return result endfunction ///Updates the value of a defined variable for a given player private function update_value takes string name, player p, string op, string value, integer val_type returns nothing local integer id = GetPlayerId(p) if p == null or id < 0 or id >= 12 then call BJDebugMsg("MMD Set Error: Invalid player. Must be P1 to P12.") elseif val_type != GetStoredInteger(gc, "types", name) then call BJDebugMsg("MMD Set Error: Updated value of undefined variable or used value of incorrect type.") elseif StringLength(op) == 0 then call BJDebugMsg("MMD Set Error: Unrecognized operation type.") elseif StringLength(name) > 50 then call BJDebugMsg("MMD Set Error: Variable name is too long.") elseif StringLength(name) == 0 then call BJDebugMsg("MMD Set Error: Variable name is empty.") else call emit("VarP " + I2S(id) + " " + pack(name) + " " + op + " " + value) endif endfunction ///Defines an event's arguments and format private function DefineEvent takes string name, integer num_args, string format, string arg_data returns nothing if GetStoredInteger(gc, "events", name) != 0 then call BJDebugMsg("MMD DefEvent Error: Event redefined.") else call StoreInteger(gc, "events", name, num_args+1) call emit("DefEvent " + pack(name) + " " + I2S(num_args) + " " + arg_data + pack(format)) endif endfunction ///Places an event in the meta-data private function LogEvent takes string name, integer num_args, string data returns nothing if GetStoredInteger(gc, "events", name) != num_args+1 then call BJDebugMsg("MMD LogEvent Error: Event not defined or defined with different # of args.") else call emit("Event " + pack(name) + data) endif endfunction ///Sets a player flag like "win_on_leave" public function FlagPlayer takes player p, integer flag_type returns nothing local string flag = flags[flag_type] local integer id = GetPlayerId(p) if p == null or id < 0 or id >= 12 then call BJDebugMsg("MMD Flag Error: Invalid player. Must be P1 to P12.") elseif StringLength(flag) == 0 then call BJDebugMsg("MMD Flag Error: Unrecognized flag type.") elseif GetPlayerController(Player(id)) == MAP_CONTROL_USER then call emit("FlagP " + I2S(id) + " " + flag) endif endfunction ///Defines a variable to store things in public function DefineValue takes string name, integer value_type, integer goal_type, integer suggestion_type returns nothing local string goal = goals[goal_type] local string vtype = types[value_type] local string stype = suggestions[suggestion_type] if goal == null then call BJDebugMsg("MMD Def Error: Unrecognized goal type.") elseif vtype == null then call BJDebugMsg("MMD Def Error: Unrecognized value type.") elseif stype == null then call BJDebugMsg("Stats Def Error: Unrecognized suggestion type.") elseif StringLength(name) > 32 then call BJDebugMsg("MMD Def Error: Variable name is too long.") elseif StringLength(name) == 0 then call BJDebugMsg("MMD Def Error: Variable name is empty.") elseif value_type == TYPE_STRING and goal_type != GOAL_NONE then call BJDebugMsg("MMD Def Error: Strings must have goal type of none.") elseif GetStoredInteger(gc, "types", name) != 0 then call BJDebugMsg("MMD Def Error: Value redefined.") else call StoreInteger(gc, "types", name, value_type) call emit("DefVarP " + pack(name) + " " + vtype + " " + goal + " " + stype) endif endfunction ///Updates the value of an integer variable public function UpdateValueInt takes string name, player p, integer op, integer value returns nothing call update_value(name, p, ops[op], I2S(value), TYPE_INT) endfunction ///Updates the value of a real variable public function UpdateValueReal takes string name, player p, integer op, real value returns nothing call update_value(name, p, ops[op], R2S(value), TYPE_REAL) endfunction ///Updates the value of a string variable public function UpdateValueString takes string name, player p, string value returns nothing local string q = "\"" call update_value(name, p, ops[OP_SET], q + pack(value) + q, TYPE_STRING) endfunction public function DefineEvent0 takes string name, string format returns nothing call DefineEvent(name, 0, format, "") endfunction public function DefineEvent1 takes string name, string format, string argName0 returns nothing call DefineEvent(name, 1, format, pack(argName0) + " ") endfunction public function DefineEvent2 takes string name, string format, string argName0, string argName1 returns nothing call DefineEvent(name, 2, format, pack(argName0) + " " + pack(argName1) + " ") endfunction public function DefineEvent3 takes string name, string format, string argName0, string argName1, string argName2 returns nothing call DefineEvent(name, 3, format, pack(argName0) + " " + pack(argName1) + " " + pack(argName2) + " ") endfunction public function LogEvent0 takes string name returns nothing call LogEvent(name, 0, "") endfunction public function LogEvent1 takes string name, string arg0 returns nothing call LogEvent(name, 1, " " + pack(arg0)) endfunction public function LogEvent2 takes string name, string arg0, string arg1 returns nothing call LogEvent(name, 2, " " + pack(arg0) + " " + pack(arg1)) endfunction public function LogEvent3 takes string name, string arg0, string arg1, string arg2 returns nothing call LogEvent(name, 3, " " + pack(arg0) + " " + pack(arg1) + " " + pack(arg2)) endfunction ///Emits meta-data which parsers will ignore unless they are customized to understand it public function LogCustom takes string unique_identifier, string data returns nothing call emit("custom " + pack(unique_identifier) + " " + pack(data)) endfunction ///Emits initialization data private function init2 takes nothing returns nothing local integer i local trigger t set initialized = true call emit("init version " + I2S(MINIMUM_PARSER_VERSION) + " " + I2S(CURRENT_VERSION)) set i = 0 loop exitwhen i >= 12 if GetPlayerController(Player(i)) == MAP_CONTROL_USER and GetPlayerSlotState(Player(i)) == PLAYER_SLOT_STATE_PLAYING then call emit("init pid " + I2S(i) + " " + pack(GetPlayerName(Player(i)))) endif set i = i + 1 endloop set t = CreateTrigger() call TriggerAddAction(t, function tick) call TriggerRegisterTimerEvent(t, 0.37, true) endfunction ///Places init2 on a timer, initializes game cache, and translates constants private function init takes nothing returns nothing local trigger t = CreateTrigger() call TriggerRegisterTimerEvent(t, 0, false) call TriggerAddAction(t, function init2) set goals[GOAL_NONE] = "none" set goals[GOAL_HIGH] = "high" set goals[GOAL_LOW] = "low" set types[TYPE_INT] = "int" set types[TYPE_REAL] = "real" set types[TYPE_STRING] = "string" set suggestions[SUGGEST_NONE] = "none" set suggestions[SUGGEST_TRACK] = "track" set suggestions[SUGGEST_LEADERBOARD] = "leaderboard" set ops[OP_ADD] = "+=" set ops[OP_SUB] = "-=" set ops[OP_SET] = "=" set flags[FLAG_DRAWER] = "drawer" set flags[FLAG_LOSER] = "loser" set flags[FLAG_WINNER] = "winner" set flags[FLAG_LEAVER] = "leaver" set flags[FLAG_PRACTICING] = "practicing" call FlushGameCache(InitGameCache(FILENAME)) set gc = InitGameCache(FILENAME) call TimerStart(clock, 999999999, false, null) call prepC2I() endfunction endlibrary |
| 02-12-2009, 03:14 AM | #2 |
Looks sort of useful. |
| 02-12-2009, 08:41 AM | #3 |
This is useless, completely, there is no replay parsers to parse "this" format of replay data. You shoud copy dota system, or provide some settings file for existing parser or new parser. |
| 02-12-2009, 03:10 PM | #4 | |
Quote:
Right, and if you read my post you would see that I mentioned that. I'm posting this system prematurely so you can COMMENT on the format and the library. But of course you can still use it to emit meta data instead of writing your own library. You apparently don't know what the dota system currently is. It is *not* generalizable to more maps. It's messages are mostly of the style "8_1", which means nothing unless you know what 8 and 1 mean (it's an item slot I think). I'm not sure what you mean by a settings file. I'm writing a parser for my bot. Varlock will write a parser for his bot. I'll also include the system in some of my maps. You have to start somewhere. |
| 02-12-2009, 04:15 PM | #5 | |
Quote:
|
| 02-12-2009, 04:27 PM | #6 | |
Quote:
The unofficial first phase of submitting a resource is taking comments, suggestions, and flak on it. I don't see why stating it explicitly makes it not a resource submission. But whatever, does anyone have any actual comments on how the system should be changed or improved? |
| 02-12-2009, 04:44 PM | #7 | ||
Quote:
Quote:
|
| 02-12-2009, 05:13 PM | #8 |
You were correct to move it. What do you think of the system? |
| 02-12-2009, 05:42 PM | #9 |
I'm trying to figure out exactly what it's supposed to be for in the first place. I read the first post and get that its application is for replays and the like, but I don't actually do any of that sort of thing so I wouldn't honestly know. |
| 02-12-2009, 05:51 PM | #10 | |
Quote:
Well, suppose you had a relatively popular map and you wanted to set up a ladder. There are hosting bots you can use to do this, but they have no reliable way of tracking things like who had the most kills. Even knowing who won and lost is not reliable, because if a player leaves between the time play ends and the game ends (for example, while an end-of-game scoreboard is being shown) there is no way for a bot to know if they won or lost. Replay and network traffic simply don't carry that information. This system is a way to output information of that nature. Any map can communicate to any bot, as long they use the standard. Currently bots need to be hard coded for the meta data a map gives out, and essentially no maps do it because unless you're as big as DOTA, who's going to bother coding a bot for you? |
| 02-12-2009, 06:16 PM | #11 |
Okay, yeah, so my guess was correct then. So you're trying to make some sort of standard for this type of thing that all maps can take advantage of? And then mapmakers would have to implement these libraries into your map in order to output the proper meta data for the bots to retrieve? I agree that a standard is an important thing, but you'd probably want to communicate that with the bot program authors rather than to us. |
| 02-12-2009, 06:55 PM | #12 | |
Quote:
I am communicating with bot makers. I have Varlock on board [maker of GHost]. Some bot programmers might wait until maps actually use it before adding support, so its important to push on both fronts. |
| 02-13-2009, 01:22 AM | #13 |
MaD Balls Arena is being hosted alot on a 100mbit bot on bnet, im trying to contact the hoster. Cus i got no idea why he host my map :P If mba somehow become more popular i will implement this system for ladder. CUs mba is truely a ladder game |
| 02-13-2009, 02:22 AM | #14 |
What is this script and what does it mean? |
| 02-13-2009, 02:58 AM | #15 |
Stop caring what the values are thrown at it; let bots worry about it. The reason being this requires wc3 to take precious processing time and the least of it you can use the most likely to be standardized. Don't use strings; use integers. If someone needs alot of information passed this will eat memory and bandwith. Also; how are you bots reading this information? It's possible you could use an array as a stack of pending things to be parsed and have it emptied every packet <not sure how you'd dump it every packet though; some sort of sync trick?> Anyways; I can see why this would be useful. Right now though I think it still needs work. |
