· 6 years ago · Feb 16, 2020, 08:20 PM
1import pathlib = require('path');
2import fs = require('fs');
3import _ = require('lodash');
4const fetch = require("node-fetch");
5var xpath = require('xpath'),
6 dom = require('xmldom').DOMParser;
7const TRACKED_GAMES: string[] = [
8 '1-S2-1-4632373' // ZC CE
9];
10export const http = <T>(req: RequestInfo): Promise<T> =>
11 fetch(req).then((res: any) => res.json());
12
13const folderUpName = (path: string) => {
14 return pathlib.normalize(pathlib.dirname(path).split(pathlib.sep).pop())
15}
16interface Event {
17 type: string;
18}
19class AsyncQueue<T> {
20 private _promises: Promise<T>[] = [];
21 private _resolvers: ((t: T) => void)[] = [];
22
23 constructor() {}
24
25 private _addPromise() {
26 this._promises.push(
27 new Promise(resolve => {
28 this._resolvers.push(resolve)
29 })
30 );
31 }
32 public enqueue(item: T) {
33 if (!this._resolvers.length) {
34 this._addPromise();
35 }
36 const resolve = this._resolvers.shift();
37 resolve(item);
38 }
39 public dequeue() {
40 if (!this._promises.length) {
41 this._addPromise();
42 }
43 const promise = this._promises.shift();
44 return promise;
45 }
46 public isEmpty() {
47 return !this._promises.length;
48 }
49}
50class EventCache<T> extends Array<T> {
51 maxSize: number = 10;
52 constructor(items?: Array<T>, maxSize: number = 10) {
53 super(...items);
54 this.maxSize = maxSize;
55 Object.setPrototypeOf(this, EventCache.prototype);
56 }
57 public add(o: T) {
58 this.push(o);
59 if (this.length > this.maxSize) {
60 this.shift();
61 }
62 }
63 public containsEvent(o: T) {
64 // TODO: Expensive. How to best
65 // check for membership of two JSON objects?
66 for (let e of this) {
67 if (_.isEqual(o, e)) {
68 return true;
69 }
70 }
71 return false;
72
73 }
74}
75class Game {
76 path: string;
77 eventFile: string;
78 id: string;
79 tracked: boolean;
80 profileId: string;
81 constructor(path: string) {
82 this.path = path;
83 this.id = path.substring(path.lastIndexOf(pathlib.sep)+1);
84 this.eventFile = pathlib.normalize(this.path + pathlib.sep + 'Events.SC2Bank')
85 this.tracked = TRACKED_GAMES.includes(this.id);
86 this.profileId = folderUpName(pathlib.dirname(this.path));
87 }
88 public lastEvent(path: string = this.eventFile) {
89 const data = fs.readFileSync(path, {encoding: 'utf8'}).toString();
90 var re_pattern = '';
91 var document = new dom().parseFromString(data, 'text/xml');
92 var search = "//Key/@name[not(. < //Key/@name)]";
93 var key = xpath.select1(search, document).value;
94 search = `//Key[@name='${key}']/Value/@text`
95 var result = xpath.select1(search, document).value;
96 result = result.replace(/,(?=\s+[]}])/g, "").replace(/`/g, '"');
97 result = JSON.parse(result);
98 // get game id and annotate
99 var gameId = xpath.select1('//Section/@name', document).value;
100 result.game_id = Number(gameId);
101 return result
102
103 }
104}
105class Client {
106 path: string;
107 games: Game[] = [];
108 queue: AsyncQueue<Event> = new AsyncQueue();
109 chokidar: Object;
110 eventCache: EventCache<Event> = new EventCache<Event>(); // To keep out duplicates
111
112 constructor(path: string) {
113 this.path = path;
114 var listeners = {
115 directories: (root: string, stats: any, next: any) => {
116 for (let stat of stats) {
117 if (stat.type != 'directory')
118 continue;
119 var statspath: string = pathlib.normalize(root + pathlib.sep + stat.name);
120 if (this._isGameDir(statspath)) {
121 this.games.push(new Game(statspath));
122 }
123 }
124 next();
125 }
126 }
127 require('walk').walkSync(path, {listeners: listeners});
128 this.dispatcher();
129 }
130 private _isGameDir(path: string) {
131 return folderUpName(path).toLowerCase() == "banks";
132 }
133 public gameFromEventPath(path: string) {
134 for (let g of this.games) {
135 if (g.eventFile == path) { return g }
136 }
137 }
138 public trackedGames() {
139 var container = [];
140 for (let g of this.games) {
141 if (g.tracked) {
142 container.push(g)
143 }
144 }
145 return container;
146 }
147 public async dispatcher() {
148 // Dispatches events in the queue to the API
149 const actions: any = {
150 match_start: matchStart,
151 player_leave: playerLeave,
152 }
153 while (true) {
154 var item = await this.queue.dequeue();
155 if (this.eventCache.containsEvent(item)) {
156 // TODO: Fix this madness. This is an expensive
157 // bruteforce way to compare two JSON objects.
158 // This is because most watchers duplicate change events.
159 continue;
160 }
161 if (actions.hasOwnProperty(item.type)) {
162 actions[item.type](item);
163 }
164 this.eventCache.add(item);
165
166 }
167 }
168 public async watch() {
169 const chokidar = require('chokidar');
170 var watcher = chokidar.watch(this.path);
171 watcher
172 .on('change', (path: string) => {
173 var game = this.gameFromEventPath(path);
174 if (game) {
175 this.queue.enqueue(game.lastEvent());
176 }
177 });
178
179 }
180}
181const matchStart = async (payload: any) => {
182 fetch('http://localhost:8000/api/automatch/', {
183 headers: {
184 'Accept': 'application/json',
185 'Content-Type': 'application/json'
186 },
187 method: 'post',
188 body: JSON.stringify(payload),
189 }).then((res: any) => {
190 console.log('api result: ' + res.status)
191 })
192 }
193
194const playerLeave = (payload: Event) => {
195 return;
196}
197
198export default Client;