diff --git a/README.md b/README.md index a9897d4..3cf4f8e 100644 --- a/README.md +++ b/README.md @@ -10,24 +10,27 @@ This parser is aimed to be more modular than other parsers. You can easily add your custom action parsing logic by overriding the processTimeSlot() function of a W3GReplay instance. -** It does not fully support replays of game version <= 1.14. ** +**It does not fully support replays of game version <= 1.14.** ## Usage ```javascript - //assuming that you cloned this repo into your project - //sorry, no npm yet ;) const W3GReplay = require('./w3gjs') const Parser = new W3GReplay() const replay = Parser.parse('./replays/sample1.w3g') ``` -### example output of the observers.w3g example replay: -```javascript - To be updated. -``` +### Example output of the observers.w3g example replay +Check the example_output.json file in the root of this repository. + +### Example demo app +You can see the parser in action here and parse your own replays aswell: +https://enigmatic-springs-58797.herokuapp.com/ + ## Contributing There is no point in hiding the implementation of tools that the community can use. So please feel free to discuss in the issues section or provide a pull request if you think you can improve this parser. +## Issues +If you have an issue using this library please use the issue section and provide an example replay file. ## License diff --git a/example_output.json b/example_output.json new file mode 100644 index 0000000..4e84252 --- /dev/null +++ b/example_output.json @@ -0,0 +1,1351 @@ +{ + "uuid": "af9420f8f5994acf58fcdb262dbd928c7e39dab13d9e58fbefcc4427e1972ece", + "gamename": "semi", + "randomseed": 1656794342, + "startSpots": 2, + "observers": [ + "hundredkg", + "Leopard", + "Edoboi", + "Hi2Chaco", + "FS_Frenzy", + "123456789012345", + "WoLv", + "pG.BLaDe" + ], + "players": [ + { + "id": 10, + "name": "u2.sok", + "teamid": 0, + "color": "#ff0000", + "race": "H", + "raceDetected": "H", + "units": { + "summary": { + "hpea": 41, + "hfoo": 12, + "hatw": 1, + "ngir": 4, + "hkee": 2, + "hgtw": 11, + "hcas": 2, + "hspt": 2, + "hgyr": 11, + "hkni": 8, + "hmtt": 1, + "hmtm": 3, + "hsor": 3, + "nzep": 2, + "hmpr": 1 + }, + "order": [ + { + "id": "hpea", + "ms": 560 + }, + { + "id": "hpea", + "ms": 640 + }, + { + "id": "hpea", + "ms": 22400 + }, + { + "id": "hpea", + "ms": 33600 + }, + { + "id": "hpea", + "ms": 44960 + }, + { + "id": "hpea", + "ms": 71040 + }, + { + "id": "hfoo", + "ms": 78720 + }, + { + "id": "hfoo", + "ms": 78880 + }, + { + "id": "hpea", + "ms": 96960 + }, + { + "id": "hfoo", + "ms": 98880 + }, + { + "id": "hfoo", + "ms": 99040 + }, + { + "id": "hpea", + "ms": 107440 + }, + { + "id": "hfoo", + "ms": 120560 + }, + { + "id": "hfoo", + "ms": 120720 + }, + { + "id": "hpea", + "ms": 134880 + }, + { + "id": "hfoo", + "ms": 147840 + }, + { + "id": "hpea", + "ms": 159120 + }, + { + "id": "hfoo", + "ms": 171680 + }, + { + "id": "hfoo", + "ms": 220960 + }, + { + "id": "hfoo", + "ms": 247760 + }, + { + "id": "hpea", + "ms": 254560 + }, + { + "id": "hpea", + "ms": 277920 + }, + { + "id": "hatw", + "ms": 292320 + }, + { + "id": "hfoo", + "ms": 310880 + }, + { + "id": "hpea", + "ms": 324160 + }, + { + "id": "hpea", + "ms": 330000 + }, + { + "id": "ngir", + "ms": 343120 + }, + { + "id": "ngir", + "ms": 343280 + }, + { + "id": "hpea", + "ms": 347920 + }, + { + "id": "hpea", + "ms": 359440 + }, + { + "id": "hkee", + "ms": 372000 + }, + { + "id": "hkee", + "ms": 372080 + }, + { + "id": "hgtw", + "ms": 398560 + }, + { + "id": "hfoo", + "ms": 407360 + }, + { + "id": "hpea", + "ms": 417360 + }, + { + "id": "hpea", + "ms": 432000 + }, + { + "id": "hgtw", + "ms": 446160 + }, + { + "id": "hgtw", + "ms": 446480 + }, + { + "id": "hgtw", + "ms": 464880 + }, + { + "id": "hgtw", + "ms": 479920 + }, + { + "id": "hgtw", + "ms": 506400 + }, + { + "id": "hcas", + "ms": 534640 + }, + { + "id": "hcas", + "ms": 534800 + }, + { + "id": "hgtw", + "ms": 555520 + }, + { + "id": "hspt", + "ms": 618080 + }, + { + "id": "hgtw", + "ms": 625200 + }, + { + "id": "hgyr", + "ms": 631040 + }, + { + "id": "hgyr", + "ms": 631200 + }, + { + "id": "hgyr", + "ms": 631280 + }, + { + "id": "hgtw", + "ms": 648400 + }, + { + "id": "hgyr", + "ms": 657760 + }, + { + "id": "hgtw", + "ms": 666960 + }, + { + "id": "hgyr", + "ms": 703760 + }, + { + "id": "hgyr", + "ms": 722320 + }, + { + "id": "hgyr", + "ms": 735760 + }, + { + "id": "hgtw", + "ms": 741760 + }, + { + "id": "hgyr", + "ms": 755120 + }, + { + "id": "hgyr", + "ms": 755840 + }, + { + "id": "hgyr", + "ms": 779680 + }, + { + "id": "hkni", + "ms": 783680 + }, + { + "id": "hgyr", + "ms": 793440 + }, + { + "id": "hkni", + "ms": 845440 + }, + { + "id": "hpea", + "ms": 862080 + }, + { + "id": "hpea", + "ms": 862160 + }, + { + "id": "hpea", + "ms": 871760 + }, + { + "id": "hpea", + "ms": 910960 + }, + { + "id": "hmtt", + "ms": 914960 + }, + { + "id": "hpea", + "ms": 922960 + }, + { + "id": "hpea", + "ms": 952000 + }, + { + "id": "hpea", + "ms": 981440 + }, + { + "id": "hpea", + "ms": 1002000 + }, + { + "id": "hpea", + "ms": 1014000 + }, + { + "id": "hpea", + "ms": 1058400 + }, + { + "id": "hpea", + "ms": 1070720 + }, + { + "id": "hkni", + "ms": 1078400 + }, + { + "id": "hpea", + "ms": 1096160 + }, + { + "id": "hkni", + "ms": 1128320 + }, + { + "id": "hspt", + "ms": 1128560 + }, + { + "id": "hpea", + "ms": 1141680 + }, + { + "id": "hpea", + "ms": 1158800 + }, + { + "id": "hkni", + "ms": 1170880 + }, + { + "id": "hpea", + "ms": 1197200 + }, + { + "id": "hpea", + "ms": 1228640 + }, + { + "id": "hpea", + "ms": 1245520 + }, + { + "id": "hpea", + "ms": 1245600 + }, + { + "id": "hpea", + "ms": 1262880 + }, + { + "id": "hmtm", + "ms": 1317120 + }, + { + "id": "hpea", + "ms": 1321120 + }, + { + "id": "hkni", + "ms": 1366640 + }, + { + "id": "hsor", + "ms": 1375600 + }, + { + "id": "hsor", + "ms": 1416160 + }, + { + "id": "ngir", + "ms": 1428880 + }, + { + "id": "ngir", + "ms": 1428960 + }, + { + "id": "nzep", + "ms": 1435040 + }, + { + "id": "nzep", + "ms": 1435200 + }, + { + "id": "hmtm", + "ms": 1442480 + }, + { + "id": "hmtm", + "ms": 1442640 + }, + { + "id": "hpea", + "ms": 1463280 + }, + { + "id": "hkni", + "ms": 1492320 + }, + { + "id": "hpea", + "ms": 1527120 + }, + { + "id": "hpea", + "ms": 1527200 + }, + { + "id": "hmpr", + "ms": 1530400 + }, + { + "id": "hkni", + "ms": 1595920 + }, + { + "id": "hsor", + "ms": 1625760 + } + ] + }, + "buildings": { + "summary": { + "halt": 2, + "hhou": 9, + "hbar": 1, + "hwtw": 20, + "htow": 2, + "hlum": 1, + "hvlt": 1, + "hbla": 1, + "hars": 1, + "harm": 2 + }, + "order": [ + { + "id": "halt", + "ms": 4400 + }, + { + "id": "halt", + "ms": 5440 + }, + { + "id": "hhou", + "ms": 16720 + }, + { + "id": "hbar", + "ms": 18400 + }, + { + "id": "hhou", + "ms": 61440 + }, + { + "id": "hhou", + "ms": 85280 + }, + { + "id": "hwtw", + "ms": 140800 + }, + { + "id": "hhou", + "ms": 161760 + }, + { + "id": "htow", + "ms": 251600 + }, + { + "id": "htow", + "ms": 254080 + }, + { + "id": "hwtw", + "ms": 258640 + }, + { + "id": "hwtw", + "ms": 261040 + }, + { + "id": "hhou", + "ms": 278640 + }, + { + "id": "hlum", + "ms": 325040 + }, + { + "id": "hvlt", + "ms": 388640 + }, + { + "id": "hwtw", + "ms": 418240 + }, + { + "id": "hwtw", + "ms": 419280 + }, + { + "id": "hwtw", + "ms": 428400 + }, + { + "id": "hwtw", + "ms": 444800 + }, + { + "id": "hhou", + "ms": 447440 + }, + { + "id": "hbla", + "ms": 458880 + }, + { + "id": "hwtw", + "ms": 465360 + }, + { + "id": "hwtw", + "ms": 466960 + }, + { + "id": "hwtw", + "ms": 467200 + }, + { + "id": "hwtw", + "ms": 467440 + }, + { + "id": "hwtw", + "ms": 480560 + }, + { + "id": "hwtw", + "ms": 481520 + }, + { + "id": "hars", + "ms": 556160 + }, + { + "id": "harm", + "ms": 561280 + }, + { + "id": "hwtw", + "ms": 597840 + }, + { + "id": "hwtw", + "ms": 598160 + }, + { + "id": "hwtw", + "ms": 625760 + }, + { + "id": "harm", + "ms": 646880 + }, + { + "id": "hwtw", + "ms": 649040 + }, + { + "id": "hwtw", + "ms": 650160 + }, + { + "id": "hwtw", + "ms": 700400 + }, + { + "id": "hhou", + "ms": 736880 + }, + { + "id": "hwtw", + "ms": 750880 + }, + { + "id": "hhou", + "ms": 1265120 + }, + { + "id": "hhou", + "ms": 1380080 + } + ] + }, + "upgrades": { + "summary": { + "Rhde": 1, + "Rhac": 4, + "Rhra": 1, + "Rhfc": 1, + "Rhan": 1, + "Rhar": 2, + "Rhme": 2, + "Rhlh": 1, + "Rhfs": 1 + }, + "order": [ + { + "id": "Rhde", + "ms": 382560 + }, + { + "id": "Rhac", + "ms": 426800 + }, + { + "id": "Rhac", + "ms": 426880 + }, + { + "id": "Rhac", + "ms": 516160 + }, + { + "id": "Rhac", + "ms": 516320 + }, + { + "id": "Rhra", + "ms": 631840 + }, + { + "id": "Rhfc", + "ms": 684000 + }, + { + "id": "Rhan", + "ms": 738560 + }, + { + "id": "Rhar", + "ms": 795760 + }, + { + "id": "Rhar", + "ms": 795840 + }, + { + "id": "Rhme", + "ms": 1162160 + }, + { + "id": "Rhme", + "ms": 1162320 + }, + { + "id": "Rhlh", + "ms": 1198800 + }, + { + "id": "Rhfs", + "ms": 1306560 + } + ] + }, + "items": { + "summary": { + "bspd": 1, + "stel": 1, + "plcl": 9, + "sreg": 6, + "phea": 3, + "ssan": 3, + "pnvl": 2 + }, + "order": [ + { + "id": "bspd", + "ms": 437840 + }, + { + "id": "stel", + "ms": 438080 + }, + { + "id": "plcl", + "ms": 510240 + }, + { + "id": "sreg", + "ms": 511360 + }, + { + "id": "phea", + "ms": 621200 + }, + { + "id": "plcl", + "ms": 621280 + }, + { + "id": "plcl", + "ms": 621440 + }, + { + "id": "phea", + "ms": 634720 + }, + { + "id": "phea", + "ms": 849200 + }, + { + "id": "sreg", + "ms": 962320 + }, + { + "id": "plcl", + "ms": 962480 + }, + { + "id": "sreg", + "ms": 1103200 + }, + { + "id": "plcl", + "ms": 1103280 + }, + { + "id": "ssan", + "ms": 1105520 + }, + { + "id": "plcl", + "ms": 1114960 + }, + { + "id": "plcl", + "ms": 1115120 + }, + { + "id": "sreg", + "ms": 1116240 + }, + { + "id": "pnvl", + "ms": 1247600 + }, + { + "id": "pnvl", + "ms": 1247760 + }, + { + "id": "sreg", + "ms": 1338720 + }, + { + "id": "plcl", + "ms": 1338880 + }, + { + "id": "ssan", + "ms": 1339120 + }, + { + "id": "sreg", + "ms": 1556880 + }, + { + "id": "plcl", + "ms": 1567440 + }, + { + "id": "ssan", + "ms": 1567840 + } + ] + }, + "heroSkills": { + "AHwe": 2, + "AHab": 2, + "AHtb": 2, + "AHhb": 3, + "AHbh": 1, + "AHds": 2 + }, + "heroes": [ + { + "level": 4, + "abilities": { + "AHwe": 2, + "AHab": 2 + }, + "id": "Hamg" + }, + { + "level": 3, + "abilities": { + "AHtb": 2, + "AHbh": 1 + }, + "id": "Hmkg" + }, + { + "level": 5, + "abilities": { + "AHhb": 3, + "AHds": 2 + }, + "id": "Hpal" + } + ], + "heroCount": 3, + "actions": { + "timed": [ + 190, + 173, + 235, + 268, + 271, + 290, + 310, + 289, + 283, + 313, + 221, + 283, + 303, + 274, + 259, + 260, + 322, + 275, + 273, + 291, + 249, + 306, + 258, + 343, + 248, + 238, + 259, + 37 + ], + "assigngroup": 1044, + "rightclick": 2597, + "basic": 98, + "ability": 134, + "item": 6, + "select": 1224, + "removeunit": 0, + "subgroup": 0, + "selecthotkey": 1789, + "esc": 9, + "buildtrain": 325 + }, + "currentTiamePlayed": 0, + "currentTimePlayed": 1628640, + "apm": 261 + }, + { + "id": 11, + "name": "Happy_", + "teamid": 1, + "color": "#0000FF", + "race": "U", + "raceDetected": "U", + "units": { + "summary": { + "uaco": 5, + "ugho": 5, + "uzg2": 1, + "unp1": 1, + "ucry": 5, + "unp2": 1, + "uobs": 12, + "uzg1": 1, + "nzep": 1, + "uabo": 4, + "umtw": 1, + "uban": 2 + }, + "order": [ + { + "id": "uaco", + "ms": 480 + }, + { + "id": "uaco", + "ms": 11760 + }, + { + "id": "uaco", + "ms": 38960 + }, + { + "id": "uaco", + "ms": 39120 + }, + { + "id": "ugho", + "ms": 91920 + }, + { + "id": "ugho", + "ms": 106000 + }, + { + "id": "uzg2", + "ms": 138560 + }, + { + "id": "unp1", + "ms": 161360 + }, + { + "id": "ucry", + "ms": 195920 + }, + { + "id": "ucry", + "ms": 222080 + }, + { + "id": "ucry", + "ms": 240480 + }, + { + "id": "ucry", + "ms": 269280 + }, + { + "id": "unp2", + "ms": 322560 + }, + { + "id": "uobs", + "ms": 415440 + }, + { + "id": "uobs", + "ms": 434240 + }, + { + "id": "uobs", + "ms": 455200 + }, + { + "id": "uzg1", + "ms": 470560 + }, + { + "id": "uobs", + "ms": 476400 + }, + { + "id": "uobs", + "ms": 503600 + }, + { + "id": "uobs", + "ms": 520720 + }, + { + "id": "nzep", + "ms": 600640 + }, + { + "id": "uobs", + "ms": 613840 + }, + { + "id": "uobs", + "ms": 677280 + }, + { + "id": "ugho", + "ms": 807280 + }, + { + "id": "ucry", + "ms": 837760 + }, + { + "id": "uabo", + "ms": 855600 + }, + { + "id": "ugho", + "ms": 857680 + }, + { + "id": "uobs", + "ms": 900480 + }, + { + "id": "uabo", + "ms": 901280 + }, + { + "id": "uabo", + "ms": 920240 + }, + { + "id": "uabo", + "ms": 939040 + }, + { + "id": "umtw", + "ms": 990480 + }, + { + "id": "ugho", + "ms": 992640 + }, + { + "id": "uobs", + "ms": 1094240 + }, + { + "id": "uban", + "ms": 1211760 + }, + { + "id": "uobs", + "ms": 1301840 + }, + { + "id": "uban", + "ms": 1323440 + }, + { + "id": "uobs", + "ms": 1528240 + }, + { + "id": "uaco", + "ms": 1565280 + } + ] + }, + "buildings": { + "summary": { + "uaod": 1, + "uzig": 6, + "usep": 1, + "utom": 1, + "ugrv": 1, + "uslh": 2, + "utod": 1 + }, + "order": [ + { + "id": "uaod", + "ms": 4080 + }, + { + "id": "uzig", + "ms": 6720 + }, + { + "id": "usep", + "ms": 31360 + }, + { + "id": "utom", + "ms": 53200 + }, + { + "id": "ugrv", + "ms": 73760 + }, + { + "id": "uzig", + "ms": 181200 + }, + { + "id": "uslh", + "ms": 337680 + }, + { + "id": "uzig", + "ms": 362640 + }, + { + "id": "uslh", + "ms": 384720 + }, + { + "id": "uzig", + "ms": 432560 + }, + { + "id": "uzig", + "ms": 563280 + }, + { + "id": "uzig", + "ms": 1126000 + }, + { + "id": "utod", + "ms": 1136240 + } + ] + }, + "upgrades": { + "summary": { + "Rusp": 1, + "Rupm": 1, + "Ruwb": 1, + "Rucr": 1, + "Rupc": 1, + "Ruba": 1 + }, + "order": [ + { + "id": "Rusp", + "ms": 463520 + }, + { + "id": "Rupm", + "ms": 695360 + }, + { + "id": "Ruwb", + "ms": 713120 + }, + { + "id": "Rucr", + "ms": 792320 + }, + { + "id": "Rupc", + "ms": 855040 + }, + { + "id": "Ruba", + "ms": 1211600 + } + ] + }, + "items": { + "summary": { + "rnec": 6, + "ocor": 1, + "pman": 1, + "pnvl": 3 + }, + "order": [ + { + "id": "rnec", + "ms": 122640 + }, + { + "id": "rnec", + "ms": 122800 + }, + { + "id": "rnec", + "ms": 344560 + }, + { + "id": "rnec", + "ms": 344720 + }, + { + "id": "ocor", + "ms": 770160 + }, + { + "id": "rnec", + "ms": 1009040 + }, + { + "id": "pman", + "ms": 1016720 + }, + { + "id": "pnvl", + "ms": 1227680 + }, + { + "id": "rnec", + "ms": 1258400 + }, + { + "id": "pnvl", + "ms": 1384720 + }, + { + "id": "pnvl", + "ms": 1384880 + } + ] + }, + "heroSkills": { + "AUdc": 3, + "AUfn": 3, + "AUau": 2, + "AUdr": 1, + "AUfu": 1, + "ANsi": 1, + "ANba": 2 + }, + "heroes": [ + { + "level": 5, + "abilities": { + "AUdc": 3, + "AUau": 2 + }, + "id": "Udea" + }, + { + "level": 5, + "abilities": { + "AUfn": 3, + "AUdr": 1, + "AUfu": 1 + }, + "id": "Ulic" + }, + { + "level": 3, + "abilities": { + "ANsi": 1, + "ANba": 2 + }, + "id": "Nbrn" + } + ], + "heroCount": 3, + "actions": { + "timed": [ + 421, + 370, + 339, + 405, + 332, + 306, + 327, + 374, + 355, + 389, + 291, + 320, + 331, + 281, + 295, + 351, + 280, + 318, + 387, + 357, + 328, + 340, + 297, + 345, + 298, + 396, + 319, + 32 + ], + "assigngroup": 347, + "rightclick": 2615, + "basic": 716, + "ability": 103, + "item": 8, + "select": 2063, + "removeunit": 1, + "subgroup": 0, + "selecthotkey": 2967, + "esc": 5, + "buildtrain": 311 + }, + "currentTiamePlayed": 0, + "currentTimePlayed": 1626480, + "apm": 328 + } + ], + "matchup": "HvU", + "creator": "GHost++", + "type": "1on1", + "chat": [], + "apm": { + "trackingInterval": 60000 + }, + "map": { + "path": "Maps\\w3arena\\w3arena__amazonia__v3.w3x", + "file": "w3arena__amazonia__v3.w3x", + "checksum": "51a1c63b" + }, + "version": "1.26", + "duration": 1632400, + "expansion": true, + "settings": { + "referees": false, + "fixedTeams": false, + "fullSharedUnitControl": false, + "alwaysVisible": true, + "hideTerrain": false, + "mapExplored": true, + "teamsTogether": false, + "randomHero": false, + "randomRaces": false, + "speed": 2 + } + } \ No newline at end of file diff --git a/lib/Player.js b/lib/Player.js index 4d096a3..f8cab1e 100644 --- a/lib/Player.js +++ b/lib/Player.js @@ -3,7 +3,7 @@ const reverseString = input => input.split('').reverse().join('') const isObjectId = input => ['u', 'e', 'h', 'o'].indexOf(input[0]) >= 0 const isRightclickAction = input => input[0] === 0x03 && input[1] === 0 const isBasicAction = input => input[0] <= 0x19 && input[1] === 0 -const {items, units, buildings, upgrades} = require('./mappings') +const {items, units, buildings, upgrades, abilityToHero} = require('./mappings') function Player (id, name, teamid, color, race) { this.id = id @@ -11,11 +11,14 @@ function Player (id, name, teamid, color, race) { this.teamid = teamid this.color = convert.playerColor(color) this.race = race - this.detectedRace = null - this.units = {} - this.upgrades = {} + this.raceDetected = false + this.units = {summary: {}, order: []} + this.buildings = {summary: {}, order: []} + this.upgrades = {summary: {}, order: []} + this.items = {summary: {}, order: []} this.heroSkills = {} - this.items = {} + this.heroes = {} + this.heroCount = 0 this.actions = { timed: [], assigngroup: 0, @@ -29,9 +32,8 @@ function Player (id, name, teamid, color, race) { selecthotkey: 0, esc: 0 } - this.buildings = {} this._currentlyTrackedAPM = 0 - this.currentTimePlayed = 0 + this.currentTiamePlayed = 0 return this } @@ -43,33 +45,47 @@ Player.prototype.newActionTrackingSegment = function (timeTrackingInterval = 600 Player.prototype.detectRaceByActionId = function (actionId) { switch (actionId[0]) { case 'e': - this.detectedRace = 'N' + this.raceDetected = 'N' break case 'o': - this.detectedRace = 'O' + this.raceDetected = 'O' break case 'h': - this.detectedRace = 'H' + this.raceDetected = 'H' break case 'u': - this.detectedRace = 'U' + this.raceDetected = 'U' break } } -Player.prototype.handleActionId = function (actionId) { +Player.prototype.handleActionId = function (actionId, gametime) { if (units[actionId]) { - this.units[actionId] = this.units[actionId] + 1 || 1 + this.units.summary[actionId] = this.units.summary[actionId] + 1 || 1 + this.units.order.push({id: actionId, ms: gametime}) } else if (items[actionId]) { - this.items[actionId] = this.items[actionId] + 1 || 1 + this.items.summary[actionId] = this.items.summary[actionId] + 1 || 1 + this.items.order.push({id: actionId, ms: gametime}) } else if (buildings[actionId]) { - this.buildings[actionId] = this.buildings[actionId] + 1 || 1 + this.buildings.summary[actionId] = this.buildings.summary[actionId] + 1 || 1 + this.buildings.order.push({id: actionId, ms: gametime}) } else if (upgrades[actionId]) { - this.upgrades[actionId] = this.upgrades[actionId] + 1 || 1 + this.upgrades.summary[actionId] = this.upgrades.summary[actionId] + 1 || 1 + this.upgrades.order.push({id: actionId, ms: gametime}) } } -Player.prototype.handle0x10 = function (actionId) { +Player.prototype.handleHeroSkill = function (actionId) { + if (this.heroes[abilityToHero[actionId]] === undefined) { + this.heroCount += 1 + this.heroes[abilityToHero[actionId]] = {level: 0, abilities: {}, order: this.heroCount, id: abilityToHero[actionId]} + } + this.heroes[abilityToHero[actionId]].level += 1 + this.heroes[abilityToHero[actionId]].abilities[actionId] = this.heroes[abilityToHero[actionId]].abilities[actionId] || 0 + this.heroes[abilityToHero[actionId]].abilities[actionId] += 1 +} + +Player.prototype.handle0x10 = function (actionId, gametime) { if (typeof actionId === 'string') { actionId = reverseString(actionId) } @@ -77,19 +93,20 @@ Player.prototype.handle0x10 = function (actionId) { switch (actionId[0]) { case 'A': this.heroSkills[actionId] = this.heroSkills[actionId] + 1 || 1 + this.handleHeroSkill(actionId, gametime) break case 'R': - this.upgrades[actionId] = this.upgrades[actionId] + 1 || 1 + this.handleActionId(actionId, gametime) break case 'u': case 'e': case 'h': case 'o': - if (!this.detectedRace) this.detectRaceByActionId(actionId) - this.handleActionId(actionId) + if (!this.raceDetected) this.detectRaceByActionId(actionId) + this.handleActionId(actionId, gametime) break default: - this.handleActionId(actionId) + this.handleActionId(actionId, gametime) } actionId[0] !== '0' @@ -99,7 +116,7 @@ Player.prototype.handle0x10 = function (actionId) { this._currentlyTrackedAPM++ } -Player.prototype.handle0x11 = function (actionId) { +Player.prototype.handle0x11 = function (actionId, gametime) { if (Array.isArray(actionId)) { if (actionId[0] <= 0x19 && actionId[1] === 0) { this.actions['basic'] = this.actions['basic'] + 1 || 1 @@ -111,7 +128,7 @@ Player.prototype.handle0x11 = function (actionId) { actionId = reverseString(actionId) } if (isObjectId(actionId)) { - this.buildings[actionId] = this.buildings[actionId] + 1 || 1 + this.handleActionId(actionId, gametime) } this._currentlyTrackedAPM++ } @@ -180,7 +197,11 @@ Player.prototype.handleOther = function (actionId) { Player.prototype.cleanup = function () { const apmSum = this.actions.timed.reduce((a, b) => a + b) this.apm = Math.round(apmSum / this.actions.timed.length) - + this.heroes = Object.values(this.heroes).sort((h1, h2) => h1.order - h2.order).reduce((aggregator, hero) => { + delete hero['order'] + aggregator.push(hero) + return aggregator + }, []) delete this._currentlyTrackedAPM } diff --git a/lib/W3GReplay.js b/lib/W3GReplay.js index 5547cda..bc08478 100644 --- a/lib/W3GReplay.js +++ b/lib/W3GReplay.js @@ -13,6 +13,7 @@ const GameDataParserComposed = new Parser() .nest('blocks', {type: GameDataParser}) const Player = require('./Player') +const crypto = require('crypto') function W3GReplay () { return this @@ -52,8 +53,7 @@ W3GReplay.prototype.parse = function (filepath) { this.processGameDataBlocks() this.cleanup() - console.timeEnd('parse') - return Object.assign({}, this) + return this.finalize() } W3GReplay.prototype.createPlayerList = function () { @@ -123,13 +123,13 @@ W3GReplay.prototype.processCommandDataBlock = function (actionBlock) { ActionBlockList.parse(actionBlock.actions).forEach(action => { switch (action.actionId) { case 0x10: - currentPlayer.handle0x10(action.itemId) + currentPlayer.handle0x10(action.itemId, this.totalTimeTracker) break case 0x11: - currentPlayer.handle0x11(action.itemId) + currentPlayer.handle0x11(action.itemId, this.totalTimeTracker) break case 0x12: - currentPlayer.handle0x12(action.itemId) + currentPlayer.handle0x12(action.itemId, this.totalTimeTracker) break case 0x13: currentPlayer.handle0x13(action.itemId) @@ -198,14 +198,31 @@ W3GReplay.prototype.determineMatchup = function () { Object.values(this.players).forEach((p) => { if (!this.isObserver(p)) { teamRaces[p.teamid] = teamRaces[p.teamid] || [] - teamRaces[p.teamid].push(p.detectedRace || p.race) + teamRaces[p.teamid].push(p.raceDetected || p.race) } }) - this.matchup = (Object.values(teamRaces).map(e => e.sort().join(''))).sort().join('v') + this.gametype = Object.values(teamRaces).map(e => e.length).sort().join('on') + this.matchup = Object.values(teamRaces).map(e => e.sort().join('')).sort().join('v') +} + +W3GReplay.prototype.generateID = function () { + let players = Object.values(this.players).filter((p) => this.isObserver(p) === false).sort((player1, player2) => { + if (player1.id < player2.id) { + return -1 + } + return 1 + }).reduce((accumulator, player) => { + accumulator += player.name + return accumulator + }, '') + + const idBase = this.meta.meta.randomSeed + players + this.meta.mapName + this.id = crypto.createHash('sha256').update(idBase).digest('hex') } W3GReplay.prototype.cleanup = function () { this.determineMatchup() + this.generateID() this.observers = [] Object.values(this.players).forEach(p => { @@ -233,4 +250,45 @@ W3GReplay.prototype.cleanup = function () { delete this.meta.blocks } +W3GReplay.prototype.finalize = function () { + const settings = { + referees: Boolean(this.meta.referees), + fixedTeams: Boolean(this.meta.fixedTeams), + fullSharedUnitControl: Boolean(this.meta.fullSharedUnitControl), + alwaysVisible: Boolean(this.meta.alwaysVisible), + hideTerrain: Boolean(this.meta.hideTerrain), + mapExplored: Boolean(this.meta.mapExplored), + teamsTogether: Boolean(this.meta.teamsTogether), + randomHero: Boolean(this.meta.randomHero), + randomRaces: Boolean(this.meta.randomRaces), + speed: this.meta.speed + } + const root = { + id: this.id, + gamename: this.meta.meta.gameName, + randomseed: this.meta.meta.randomSeed, + startSpots: this.meta.meta.startSpotCount, + observers: this.observers, + players: Object.values(this.players).sort((player1, player2) => player2.teamid >= player1.teamid && player2.id > player1.id ? -1 : 1), + matchup: this.matchup, + creator: this.meta.creator, + type: this.gametype, + chat: [], + apm: { + trackingInterval: this.playerActionTrackInterval + }, + map: { + path: this.meta.mapName, + file: this.meta.mapName.split('\\').pop(), + checksum: this.meta.mapChecksum + }, + version: `1.${this.header.version}`, + duration: this.header.replayLengthMS, + expansion: this.header.gameIdentifier === 'PX3W', + settings + } + + return root +} + module.exports = W3GReplay diff --git a/lib/mappings.js b/lib/mappings.js index b5dbc71..4a76c5b 100644 --- a/lib/mappings.js +++ b/lib/mappings.js @@ -533,10 +533,232 @@ const upgrades = { 'Rupm': 'p_Backpack' } +const heroAbilities = { + 'AHbz': 'a_Archmage:Blizzard', + 'AHwe': 'a_Archmage:Summon Water Elemental', + 'AHab': 'a_Archmage:Brilliance Aura', + 'AHmt': 'a_Archmage:Mass Teleport', + 'AHtb': 'a_Mountain King:Storm Bolt', + 'AHtc': 'a_Mountain King:Thunder Clap', + 'AHbh': 'a_Mountain King:Bash', + 'AHav': 'a_Mountain King:Avatar', + 'AHhb': 'a_Paladin:Holy Light', + 'AHds': 'a_Paladin:Divine Shield', + 'AHad': 'a_Paladin:Devotion Aura', + 'AHre': 'a_Paladin:Resurrection', + 'AHdr': 'a_Blood Mage:Siphon Mana', + 'AHfs': 'a_Blood Mage:Flame Strike', + 'AHbn': 'a_Blood Mage:Banish', + 'AHpx': 'a_Blood Mage:Summon Phoenix', + 'AEmb': 'a_Demon Hunter:Mana Burn', + 'AEim': 'a_Demon Hunter:Immolation', + 'AEev': 'a_Demon Hunter:Evasion', + 'AEme': 'a_Demon Hunter:Metamorphosis', + 'AEer': 'a_Keeper of the Grove:Entangling Roots', + 'AEfn': 'a_Keeper of the Grove:Force of Nature', + 'AEah': 'a_Keeper of the Grove:Thorns Aura', + 'AEtq': 'a_Keeper of the Grove:Tranquility', + 'AEst': 'a_Priestess of the Moon:Scout', + 'AHfa': 'a_Priestess of the Moon:Searing Arrows', + 'AEar': 'a_Priestess of the Moon:Trueshot Aura', + 'AEsf': 'a_Priestess of the Moon:Starfall', + 'AEbl': 'a_Warden:Blink', + 'AEfk': 'a_Warden:Fan of Knives', + 'AEsh': 'a_Warden:Shadow Strike', + 'AEsv': 'a_Warden:Spirit of Vengeance', + 'AOwk': 'a_Blademaster:Wind Walk', + 'AOmi': 'a_Blademaster:Mirror Image', + 'AOcr': 'a_Blademaster:Critical Strike', + 'AOww': 'a_Blademaster:Bladestorm', + 'AOcl': 'a_Far Seer:Chain Lighting', + 'AOfs': 'a_Far Seer:Far Sight', + 'AOsf': 'a_Far Seer:Feral Spirit', + 'AOeq': 'a_Far Seer:Earth Quake', + 'AOsh': 'a_Tauren Chieftain:Shockwave', + 'AOae': 'a_Tauren Chieftain:Endurance Aura', + 'AOws': 'a_Tauren Chieftain:War Stomp', + 'AOre': 'a_Tauren Chieftain:Reincarnation', + 'AOhw': 'a_Shadow Hunter:Healing Wave', + 'AOhx': 'a_Shadow Hunter:Hex', + 'AOsw': 'a_Shadow Hunter:Serpent Ward', + 'AOvd': 'a_Shadow Hunter:Big Bad Voodoo', + 'AUdc': 'a_Death Knight:Death Coil', + 'AUdp': 'a_Death Knight:Death Pact', + 'AUau': 'a_Death Knight:Unholy Aura', + 'AUan': 'a_Death Knight:Animate Dead', + 'AUcs': 'a_Dreadlord:Carrion Swarm', + 'AUsl': 'a_Dreadlord:Sleep', + 'AUav': 'a_Dreadlord:Vampiric Aura', + 'AUin': 'a_Dreadlord:Inferno', + 'AUfn': 'a_Lich:Frost Nova', + 'AUfa': 'a_Lich:Frost Armor', + 'AUfu': 'a_Lich:Frost Armor', + 'AUdr': 'a_Lich:Dark Ritual', + 'AUdd': 'a_Lich:Death and Decay', + 'AUim': 'a_Crypt Lord:Impale', + 'AUts': 'a_Crypt Lord:Spiked Carapace', + 'AUcb': 'a_Crypt Lord:Carrion Beetles', + 'AUls': 'a_Crypt Lord:Locust Swarm', + 'ANbf': 'a_Pandaren Brewmaster:Breath of Fire', + 'ANdb': 'a_Pandaren Brewmaster:Drunken Brawler', + 'ANdh': 'a_Pandaren Brewmaster:Drunken Haze', + 'ANef': 'a_Pandaren Brewmaster:Storm Earth and Fire', + 'ANdr': 'a_Dark Ranger:Life Drain', + 'ANsi': 'a_Dark Ranger:Silence', + 'ANba': 'a_Dark Ranger:Black Arrow', + 'ANch': 'a_Dark Ranger:Charm', + 'ANms': 'a_Naga Sea Witch:Mana Shield', + 'ANfa': 'a_Naga Sea Witch:Frost Arrows', + 'ANfl': 'a_Naga Sea Witch:Forked Lightning', + 'ANto': 'a_Naga Sea Witch:Tornado', + 'ANrf': 'a_Pit Lord:Rain of Fire', + 'ANca': 'a_Pit Lord:Cleaving Attack', + 'ANht': 'a_Pit Lord:Howl of Terror', + 'ANdo': 'a_Pit Lord:Doom', + 'ANsg': 'a_Beastmaster:Summon Bear', + 'ANsq': 'a_Beastmaster:Summon Quilbeast', + 'ANsw': 'a_Beastmaster:Summon Hawk', + 'ANst': 'a_Beastmaster:Stampede', + 'ANeg': 'a_Goblin Tinker:Engineering Upgrade', + 'ANcs': 'a_Goblin Tinker:Cluster Rockets', + 'ANc1': 'a_Goblin Tinker:Cluster Rockets', + 'ANc2': 'a_Goblin Tinker:Cluster Rockets', + 'ANc3': 'a_Goblin Tinker:Cluster Rockets', + 'ANsy': 'a_Goblin Tinker:Pocket Factory', + 'ANs1': 'a_Goblin Tinker:Pocket Factory', + 'ANs2': 'a_Goblin Tinker:Pocket Factory', + 'ANs3': 'a_Goblin Tinker:Pocket Factory', + 'ANrg': 'a_Goblin Tinker:Robo-Goblin', + 'ANg1': 'a_Goblin Tinker:Robo-Goblin', + 'ANg2': 'a_Goblin Tinker:Robo-Goblin', + 'ANg3': 'a_Goblin Tinker:Robo-Goblin', + 'ANic': 'a_Firelord:Incinerate', + 'ANia': 'a_Firelord:Incinerate', + 'ANso': 'a_Firelord:Soul Burn', + 'ANlm': 'a_Firelord:Summon Lava Spawn', + 'ANvc': 'a_Firelord:Volcano', + 'ANhs': 'a_Goblin Alchemist:Healing Spray', + 'ANab': 'a_Goblin Alchemist:Acid Bomb', + 'ANcr': 'a_Goblin Alchemist:Chemical Rage', + 'ANtm': 'a_Goblin Alchemist:Transmute' +} + +const abilityToHero = { + 'AHbz': 'Hamg', + 'AHwe': 'Hamg', + 'AHab': 'Hamg', + 'AHmt': 'Hamg', + 'AHtb': 'Hmkg', + 'AHtc': 'Hmkg', + 'AHbh': 'Hmkg', + 'AHav': 'Hmkg', + 'AHhb': 'Hpal', + 'AHds': 'Hpal', + 'AHad': 'Hpal', + 'AHre': 'Hpal', + 'AHdr': 'Hblm', + 'AHfs': 'Hblm', + 'AHbn': 'Hblm', + 'AHpx': 'Hblm', + 'AEmb': 'Edem', + 'AEim': 'Edem', + 'AEev': 'Edem', + 'AEme': 'Edem', + 'AEer': 'Ekee', + 'AEfn': 'Ekee', + 'AEah': 'Ekee', + 'AEtq': 'Ekee', + 'AEst': 'Emoo', + 'AHfa': 'Emoo', + 'AEar': 'Emoo', + 'AEsf': 'Emoo', + 'AEbl': 'Ewar', + 'AEfk': 'Ewar', + 'AEsh': 'Ewar', + 'AEsv': 'Ewar', + 'AOwk': 'Obla', + 'AOmi': 'Obla', + 'AOcr': 'Obla', + 'AOww': 'Obla', + 'AOcl': 'Ofar', + 'AOfs': 'Ofar', + 'AOsf': 'Ofar', + 'AOeq': 'Ofar', + 'AOsh': 'Otch', + 'AOae': 'Otch', + 'AOws': 'Otch', + 'AOre': 'Otch', + 'AOhw': 'Oshd', + 'AOhx': 'Oshd', + 'AOsw': 'Oshd', + 'AOvd': 'Oshd', + 'AUdc': 'Udea', + 'AUdp': 'Udea', + 'AUau': 'Udea', + 'AUan': 'Udea', + 'AUcs': 'Udre', + 'AUsl': 'Udre', + 'AUav': 'Udre', + 'AUin': 'Udre', + 'AUfn': 'Ulic', + 'AUfa': 'Ulic', + 'AUfu': 'Ulic', + 'AUdr': 'Ulic', + 'AUdd': 'Ulic', + 'AUim': 'Ucrl', + 'AUts': 'Ucrl', + 'AUcb': 'Ucrl', + 'AUls': 'Ucrl', + 'ANbf': 'Npbm', + 'ANdb': 'Npbm', + 'ANdh': 'Npbm', + 'ANef': 'Npbm', + 'ANdr': 'Nbrn', + 'ANsi': 'Nbrn', + 'ANba': 'Nbrn', + 'ANch': 'Nbrn', + 'ANms': 'Nngs', + 'ANfa': 'Nngs', + 'ANfl': 'Nngs', + 'ANto': 'Nngs', + 'ANrf': 'Nplh', + 'ANca': 'Nplh', + 'ANht': 'Nplh', + 'ANdo': 'Nplh', + 'ANsg': 'Nbst', + 'ANsq': 'Nbst', + 'ANsw': 'Nbst', + 'ANst': 'Nbst', + 'ANeg': 'Ntin', + 'ANcs': 'Ntin', + 'ANc1': 'Ntin', + 'ANc2': 'Ntin', + 'ANc3': 'Ntin', + 'ANsy': 'Ntin', + 'ANs1': 'Ntin', + 'ANs2': 'Ntin', + 'ANs3': 'Ntin', + 'ANrg': 'Ntin', + 'ANg1': 'Ntin', + 'ANg2': 'Ntin', + 'ANg3': 'Ntin', + 'ANic': 'Nfir', + 'ANia': 'Nfir', + 'ANso': 'Nfir', + 'ANlm': 'Nfir', + 'ANvc': 'Nfir', + 'ANhs': 'Nalc', + 'ANab': 'Nalc', + 'ANcr': 'Nalc', + 'ANtm': 'Nalc' +} + module.exports = { items, units, buildings, upgrades, - itemIds: Object.keys(items) + heroAbilities, + itemIds: Object.keys(items), + abilityToHero } diff --git a/package.json b/package.json index 5bdde43..12b7f95 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,8 @@ "eslint-plugin-node": "^6.0.1", "eslint-plugin-promise": "^3.7.0", "eslint-plugin-standard": "^3.1.0", - "jest": "^23.5.0" + "jest": "^23.5.0", + "jsonschema": "^1.2.4" }, "scripts": { "test": "jest", diff --git a/test/schema.json b/test/schema.json new file mode 100644 index 0000000..4296762 --- /dev/null +++ b/test/schema.json @@ -0,0 +1,660 @@ +{ + "definitions": {}, + "$schema": "http://json-schema.org/draft-07/schema#", + "$id": "http://example.com/root.json", + "type": "object", + "title": "The Root Schema", + "additionalProperties": false, + "required": [ + "players", + "gamename", + "matchup", + "id", + "type", + "creator", + "map", + "version", + "settings", + "observers", + "chat" + ], + "properties": { + "players": { + "$id": "#/properties/players", + "type": "array", + "title": "The Players Schema", + "items": { + "$id": "#/properties/players/items", + "type": "object", + "title": "The Items Schema", + "required": [ + "name", + "race", + "raceDetected", + "color", + "units", + "teamid", + "id", + "buildings", + "upgrades", + "items", + "heroes" + ], + "properties": { + "heroes": { + "$id": "#/properties/players/items/properties/heroes", + "type": "array", + "title": "The Heroes Schema", + + "items": { + "$id": "#/properties/players/items/properties/heroes/items", + "type": "object", + "title": "The Items Schema", + "additionalProperties": false, + "required": [ + "level", + "abilities", + "id" + ], + "properties": { + "id": { + "$id": "#/properties/players/items/properties/heroes/items/properties/id", + "type": "string", + "title": "The id Schema" + }, + "level": { + "$id": "#/properties/players/items/properties/heroes/items/properties/level", + "type": "integer", + "title": "The level Schema" + }, + "abilities": { + "$id": "#/properties/players/items/properties/heroes/items/properties/abilities", + "type": "object", + "title": "The hero abilities schema" + } + } + } + }, + "name": { + "$id": "#/properties/players/items/properties/name", + "type": "string", + "title": "The Name Schema", + "default": "", + "examples": [ + "" + ], + "pattern": "^(.*)$" + }, + "apm": { + "$id": "#/properties/players/items/properties/apm", + "type": "integer", + "title": "Average APM of the player", + "default": "", + "examples": [ + 180 + ], + "pattern": "^(.*)$" + }, + "race": { + "$id": "#/properties/players/items/properties/race", + "type": "string", + "title": "The Race Schema", + "default": "", + "examples": [ + "" + ], + "pattern": "^(.*)$" + }, + "raceDetected": { + "$id": "#/properties/players/items/properties/raceDetected", + "type": "string", + "title": "The Racedetected Schema", + "default": "", + "examples": [ + "" + ], + "pattern": "^(.*)$" + }, + "color": { + "$id": "#/properties/players/items/properties/color", + "type": "string", + "title": "The Color Schema", + "default": "", + "examples": [ + "" + ], + "pattern": "^(.*)$" + }, + "units": { + "$id": "#/properties/players/items/properties/units", + "type": "object", + "title": "The Units Schema", + "required": [ + "summary", + "order" + ], + "properties": { + "summary": { + "$id": "#/properties/players/items/properties/units/properties/summary", + "type": "object", + "title": "The Summary Schema", + "properties": {} + }, + "order": { + "$id": "#/properties/players/items/properties/units/properties/order", + "type": "array", + "title": "The Order Schema", + "items": { + "$id": "#/properties/players/items/properties/units/properties/order/items", + "type": "object", + "title": "The Items Schema", + "required": [ + "ms", + "id" + ], + "properties": { + "ms": { + "$id": "#/properties/players/items/properties/units/properties/order/items/properties/ms", + "type": "integer", + "title": "The Ms Schema", + "default": 0, + "examples": [ + 1500 + ] + }, + "id": { + "$id": "#/properties/players/items/properties/units/properties/order/items/properties/id", + "type": "string", + "title": "The Id Schema", + "default": "", + "examples": [ + "hfoo" + ], + "pattern": "^(.*)$" + } + } + } + } + } + }, + "teamid": { + "$id": "#/properties/players/items/properties/teamid", + "type": "integer", + "title": "The Teamid Schema", + "default": 0, + "examples": [ + 2 + ] + }, + "id": { + "$id": "#/properties/players/items/properties/id", + "type": "integer", + "title": "The Id Schema", + "default": 0, + "examples": [ + 0 + ] + }, + "buildings": { + "$id": "#/properties/players/items/properties/buildings", + "type": "object", + "title": "The Buildings Schema", + "required": [ + "summary", + "order" + ], + "properties": { + "summary": { + "$id": "#/properties/players/items/properties/buildings/properties/summary", + "type": "object", + "title": "The Summary Schema" + }, + "order": { + "$id": "#/properties/players/items/properties/buildings/properties/order", + "type": "array", + "title": "The Order Schema", + "items": { + "$id": "#/properties/players/items/properties/buildings/properties/order/items", + "type": "object", + "title": "The Items Schema", + "required": [ + "ms", + "id" + ], + "properties": { + "ms": { + "$id": "#/properties/players/items/properties/buildings/properties/order/items/properties/ms", + "type": "integer", + "title": "The Ms Schema", + "default": 0, + "examples": [ + 1500 + ] + }, + "id": { + "$id": "#/properties/players/items/properties/buildings/properties/order/items/properties/id", + "type": "string", + "title": "The Id Schema", + "default": "", + "examples": [ + "hfoo" + ], + "pattern": "^(.*)$" + } + } + } + } + } + }, + "upgrades": { + "$id": "#/properties/players/items/properties/upgrades", + "type": "object", + "title": "The Upgrades Schema", + "required": [ + "summary", + "order" + ], + "properties": { + "summary": { + "$id": "#/properties/players/items/properties/upgrades/properties/summary", + "type": "object", + "title": "The Summary Schema" + }, + "order": { + "$id": "#/properties/players/items/properties/upgrades/properties/order", + "type": "array", + "title": "The Order Schema", + "items": { + "$id": "#/properties/players/items/properties/upgrades/properties/order/items", + "type": "object", + "title": "The Items Schema", + "required": [ + "ms", + "id" + ], + "properties": { + "ms": { + "$id": "#/properties/players/items/properties/upgrades/properties/order/items/properties/ms", + "type": "integer", + "title": "The Ms Schema", + "default": 0, + "examples": [ + 1500 + ] + }, + "id": { + "$id": "#/properties/players/items/properties/upgrades/properties/order/items/properties/id", + "type": "string", + "title": "The Id Schema", + "default": "", + "examples": [ + "hfoo" + ], + "pattern": "^(.*)$" + } + } + } + } + } + }, + "items": { + "$id": "#/properties/players/items/properties/items", + "type": "object", + "title": "The Items Schema", + "required": [ + "summary", + "order" + ], + "properties": { + "summary": { + "$id": "#/properties/players/items/properties/items/properties/summary", + "type": "object", + "title": "The Summary Schema" + }, + "order": { + "$id": "#/properties/players/items/properties/items/properties/order", + "type": "array", + "title": "The Order Schema", + "items": { + "$id": "#/properties/players/items/properties/items/properties/order/items", + "type": "object", + "title": "The Items Schema", + "required": [ + "ms", + "id" + ], + "properties": { + "ms": { + "$id": "#/properties/players/items/properties/items/properties/order/items/properties/ms", + "type": "integer", + "title": "The Ms Schema", + "default": 0, + "examples": [ + 1500 + ] + }, + "id": { + "$id": "#/properties/players/items/properties/items/properties/order/items/properties/id", + "type": "string", + "title": "The Id Schema", + "default": "", + "examples": [ + "hfoo" + ], + "pattern": "^(.*)$" + } + } + } + } + } + } + } + } + }, + "randomseed": { + "$id": "#/properties/randomseed", + "type": "integer", + "title": "The randomseed Schema", + "default": "" + }, + "apm": { + "$id": "#/properties/apm", + "type": "object", + "title": "The apm information Schema", + "required": ["trackingInterval"], + "properties": { + "trackingInterval": { + "$id": "#/properties/apm/properties/trackingInterval", + "type": "integer", + "title": "apm tracking interval in ms" + } + } + }, + "expansion": { + "$id": "#/properties/expansion", + "type": "boolean", + "title": "The expansion Schema", + "default": false + }, + "startSpots": { + "$id": "#/properties/startSpots", + "type": "integer", + "title": "The startSpots Schema", + "default": "", + "examples": [ + 4 + ], + "pattern": "^(.*)$" + }, + "duration": { + "$id": "#/properties/duration", + "type": "integer", + "title": "The duration schema", + "default": "", + "pattern": "^(.*)$" + }, + "gamename": { + "$id": "#/properties/gamename", + "type": "string", + "title": "The Gamename Schema", + "default": "", + "examples": [ + "" + ], + "pattern": "^(.*)$" + }, + "matchup": { + "$id": "#/properties/matchup", + "type": "string", + "title": "The Matchup Schema", + "default": "", + "examples": [ + "a well defined, universal matchup string" + ], + "pattern": "^(.*)$" + }, + "id": { + "$id": "#/properties/id", + "type": "string", + "title": "The id Schema", + "default": "", + "examples": [ + "A (not necessarily guaranteed) unique identifier for the game played in this replay. Edge cases with duplicates can exist." + ], + "pattern": "^(.*)$" + }, + "type": { + "$id": "#/properties/type", + "type": "string", + "title": "The Type Schema", + "default": "", + "examples": [ + "1vs1" + ], + "pattern": "^(.*)$" + }, + "creator": { + "$id": "#/properties/creator", + "type": "string", + "title": "The Creator Schema", + "default": "", + "examples": [ + "BNet" + ], + "pattern": "^(.*)$" + }, + "map": { + "$id": "#/properties/map", + "type": "object", + "title": "The Map Schema", + "required": [ + "path", + "file" + ], + "properties": { + "path": { + "$id": "#/properties/map/properties/path", + "type": "string", + "title": "The Path Schema", + "default": "", + "examples": [ + "Maps\\FrozenThrone\\(4)TwistedMeadows.w3x" + ], + "pattern": "^(.*)$" + }, + "file": { + "$id": "#/properties/map/properties/file", + "type": "string", + "title": "The File Schema", + "default": "", + "examples": [ + "(4)TwistedMeadows.w3x" + ], + "pattern": "^(.*)$" + } + } + }, + "version": { + "$id": "#/properties/version", + "type": "string", + "title": "The Version Schema", + "default": "", + "examples": [ + "1.31" + ], + "pattern": "^(.*)$" + }, + "settings": { + "$id": "#/properties/settings", + "type": "object", + "title": "The Settings Schema", + "required": [ + "hideTerrain", + "mapExplored", + "alwaysVisible", + "teamsTogether", + "randomHero", + "randomRaces", + "referees", + "fixedTeams", + "fullSharedUnitControl", + "speed" + ], + "properties": { + "hideTerrain": { + "$id": "#/properties/settings/properties/hideTerrain", + "type": "boolean", + "title": "The Hideterrain Schema", + "default": false, + "examples": [ + true + ] + }, + "mapExplored": { + "$id": "#/properties/settings/properties/mapExplored", + "type": "boolean", + "title": "The Mapexplored Schema", + "default": false, + "examples": [ + true + ] + }, + "alwaysVisible": { + "$id": "#/properties/settings/properties/alwaysVisible", + "type": "boolean", + "title": "The Alwaysvisible Schema", + "default": false, + "examples": [ + true + ] + }, + "teamsTogether": { + "$id": "#/properties/settings/properties/teamsTogether", + "type": "boolean", + "title": "The Teamstogether Schema", + "default": false, + "examples": [ + false + ] + }, + "randomHero": { + "$id": "#/properties/settings/properties/randomHero", + "type": "boolean", + "title": "The Randomhero Schema", + "default": false, + "examples": [ + false + ] + }, + "randomRaces": { + "$id": "#/properties/settings/properties/randomRaces", + "type": "boolean", + "title": "The Randomraces Schema", + "default": false, + "examples": [ + false + ] + }, + "referees": { + "$id": "#/properties/settings/properties/referees", + "type": "boolean", + "title": "The Referees Schema", + "default": false, + "examples": [ + false + ] + }, + "fixedTeams": { + "$id": "#/properties/settings/properties/fixedTeams", + "type": "boolean", + "title": "The Fixedteams Schema", + "default": false, + "examples": [ + true + ] + }, + "fullSharedUnitControl": { + "$id": "#/properties/settings/properties/fullSharedUnitControl", + "type": "boolean", + "title": "The Fullsharedunitcontrol Schema", + "default": false, + "examples": [ + true + ] + }, + "speed": { + "$id": "#/properties/settings/properties/speed", + "type": "integer", + "title": "The Speed Schema", + "default": 0, + "examples": [ + 2 + ] + } + } + }, + "observers": { + "$id": "#/properties/observers", + "type": "array", + "title": "The Observers Schema", + "items": { + "$id": "#/properties/observers/items", + "type": "string", + "title": "The Items Schema", + "default": "", + "examples": [ + "observer1", + "observer2" + ], + "pattern": "^(.*)$" + } + }, + "chat": { + "$id": "#/properties/chat", + "type": "array", + "title": "The Chat Schema", + "items": { + "$id": "#/properties/chat/items", + "type": "object", + "title": "The Items Schema", + "required": [ + "mode", + "player", + "message" + ], + "properties": { + "mode": { + "$id": "#/properties/chat/items/properties/mode", + "type": "string", + "title": "The Mode Schema", + "default": "", + "examples": [ + "ALL|ALLY|OBS|REF|PRIVATE" + ], + "pattern": "^(.*)$" + }, + "player": { + "$id": "#/properties/chat/items/properties/player", + "type": "string", + "title": "The Player Schema", + "default": "", + "examples": [ + "a playername" + ], + "pattern": "^(.*)$" + }, + "message": { + "$id": "#/properties/chat/items/properties/message", + "type": "string", + "title": "The Message Schema", + "default": "", + "examples": [ + "the chat message" + ], + "pattern": "^(.*)$" + } + } + } + } + } +} \ No newline at end of file diff --git a/test/test.js b/test/test.js index 2a31393..57401ce 100644 --- a/test/test.js +++ b/test/test.js @@ -1,84 +1,90 @@ const W3GReplay = require('../index') const Parser = new W3GReplay() +const {Validator} = require('jsonschema') describe('Replay parsing tests', () => { it('parses a standard 1.29 replay with observers properly', () => { const test = Parser.parse(`./replays/standard_129_obs.w3g`) - expect(test.header.magic).toBe('Warcraft III recorded game\u001a') - expect(test.header.version).toBe(29) - expect(test.players['4'].name).toBe('S.o.K.o.L') - expect(test.players['4'].detectedRace).toBe('O') - expect(test.players['4'].color).toBe('#50c878') - expect(test.players['6'].name).toBe('Stormhoof') - expect(test.players['6'].detectedRace).toBe('O') - expect(test.players['6'].color).toBe('#800000') + expect(test.version).toBe('1.29') + expect(test.players[1].name).toBe('S.o.K.o.L') + expect(test.players[1].raceDetected).toBe('O') + expect(test.players[1].id).toBe(4) + expect(test.players[1].teamid).toBe(3) + expect(test.players[1].color).toBe('#50c878') + expect(test.players[0].name).toBe('Stormhoof') + expect(test.players[0].raceDetected).toBe('O') + expect(test.players[0].color).toBe('#800000') + expect(test.players[0].id).toBe(6) + expect(test.players[0].teamid).toBe(0) expect(test.observers.length).toBe(4) - expect(test.teams['24']).toBe(undefined) expect(test.matchup).toBe('OvO') - expect(Object.keys(test.players).length).toBe(2) - expect(test.chatlog[0]).toEqual(expect.objectContaining({ - playerName: expect.any(String), - chatMode: expect.any(String), - message: expect.any(String) - })) + expect(test.type).toBe('1on1') + expect(test.players.length).toBe(2) }) it('parses a standard 1.26 replay properly', () => { const test = Parser.parse(`./replays/standard_126.w3g`) - expect(test.header.magic).toBe('Warcraft III recorded game\u001a') - expect(test.header.version).toBe(26) + expect(test.version).toBe('1.26') expect(test.observers.length).toBe(8) - expect(test.players['11'].name).toBe('Happy_') - expect(test.players['11'].detectedRace).toBe('U') - expect(test.players['11'].color).toBe('#0000FF') - expect(test.players['10'].name).toBe('u2.sok') - expect(test.players['10'].detectedRace).toBe('H') - expect(test.players['10'].color).toBe('#ff0000') - expect(test.teams['12']).toBe(undefined) + expect(test.players[1].name).toBe('Happy_') + expect(test.players[1].raceDetected).toBe('U') + expect(test.players[1].color).toBe('#0000FF') + expect(test.players[0].name).toBe('u2.sok') + expect(test.players[0].raceDetected).toBe('H') + expect(test.players[0].color).toBe('#ff0000') expect(test.matchup).toBe('HvU') - expect(Object.keys(test.players).length).toBe(2) + expect(test.type).toBe('1on1') + expect(test.players.length).toBe(2) }) it('parses a netease 1.29 replay properly', () => { const test = Parser.parse(`./replays/netease_129_obs.nwg`) - expect(test.header.magic).toBe('Warcraft III recorded game\u001a') - expect(test.header.version).toBe(29) + expect(test.version).toBe('1.29') - expect(test.players['3'].name).toBe('rudan') - expect(test.players['3'].color).toBe('#3eb489') + expect(test.players[1].name).toBe('rudan') + expect(test.players[1].color).toBe('#3eb489') expect(test.observers.length).toBe(1) - expect(test.teams['24']).toBe(undefined) expect(test.matchup).toBe('NvN') - expect(Object.keys(test.players).length).toBe(2) + expect(test.type).toBe('1on1') + expect(test.players.length).toBe(2) }) it('parses a 2on2standard 1.29 replay properly', () => { const test = Parser.parse(`./replays/999.w3g`) - expect(test.header.magic).toBe('Warcraft III recorded game\u001a') - expect(test.header.version).toBe(26) + expect(test.version).toBe('1.26') expect(test.matchup).toBe('HUvHU') - expect(Object.keys(test.players).length).toBe(4) + expect(test.type).toBe('2on2') + expect(test.players.length).toBe(4) }) it('parses a standard 1.30 replay properly', () => { const test = Parser.parse(`./replays/standard_130.w3g`) - expect(test.header.magic).toBe('Warcraft III recorded game\u001a') - expect(test.header.version).toBe(30) + expect(test.version).toBe('1.30') expect(test.matchup).toBe('NvU') - expect(test.players['3'].name).toBe('sheik') - expect(test.players['3'].race).toBe('U') - expect(test.players['3'].detectedRace).toBe('U') - expect(test.players['5'].name).toBe('123456789012345') - expect(test.players['5'].race).toBe('N') - expect(test.players['5'].detectedRace).toBe('N') - expect(Object.keys(test.players).length).toBe(2) + expect(test.type).toBe('1on1') + expect(test.players[0].name).toBe('sheik') + expect(test.players[0].race).toBe('U') + expect(test.players[0].raceDetected).toBe('U') + expect(test.players[1].name).toBe('123456789012345') + expect(test.players[1].race).toBe('N') + expect(test.players[1].raceDetected).toBe('N') + expect(test.players.length).toBe(2) + expect(test.players[0].heroes[0]).toEqual(expect.objectContaining({id: 'Udea', level: 6})) + expect(test.players[0].heroes[1]).toEqual(expect.objectContaining({id: 'Ulic', level: 6})) + expect(test.players[0].heroes[2]).toEqual(expect.objectContaining({id: 'Udre', level: 3})) + }) + + it('parsing result has the correct schema', () => { + const schema = require('./schema') + const test = Parser.parse(`./replays/standard_130.w3g`) + const validatorInstance = new Validator() + validatorInstance.validate(test, schema, {throwError: true}) }) it('parses a standard 1.30.2 replay properly', () => { const test = Parser.parse(`./replays/standard_1.302.w3g`) - expect(test.header.magic).toBe('Warcraft III recorded game\u001a') - expect(test.header.version).toBe(10030) + expect(test.version).toBe('1.10030') expect(test.matchup).toBe('NvU') - expect(Object.keys(test.players).length).toBe(2) + expect(test.players.length).toBe(2) }) })