@colyseus/monitor 0.12.2 → 0.12.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -0
- package/lib/api.d.ts +2 -2
- package/lib/api.js +88 -87
- package/lib/ext/Room.d.ts +1 -1
- package/lib/ext/Room.js +69 -69
- package/lib/index.d.ts +11 -11
- package/lib/index.js +21 -20
- package/package.json +3 -6
- package/tsconfig.json +1 -0
- package/lib/static/bundle.js +0 -43
- package/lib/static/bundle.js.map +0 -1
- package/lib/static/index.html +0 -13
- package/lib/static/lib/api.d.ts +0 -2
- package/lib/static/lib/ext/Room.d.ts +0 -1
- package/lib/static/lib/frontend/App.d.ts +0 -2
- package/lib/static/lib/frontend/components/CAppBar.d.ts +0 -8
- package/lib/static/lib/frontend/components/RoomInspect.d.ts +0 -28
- package/lib/static/lib/frontend/components/RoomList.d.ts +0 -26
- package/lib/static/lib/frontend/index.d.ts +0 -1
- package/lib/static/lib/frontend/services.d.ts +0 -4
- package/lib/static/lib/index.d.ts +0 -11
- package/lib/static/styles.css +0 -5
- package/lib/static/styles.css.map +0 -1
package/README.md
CHANGED
|
@@ -54,6 +54,10 @@ npm start
|
|
|
54
54
|
|
|
55
55
|
Access the UI on [http://localhost:2567/colyseus](http://localhost:2567/colyseus).
|
|
56
56
|
|
|
57
|
+
## Environment Variables
|
|
58
|
+
|
|
59
|
+
* `GAME_SERVER_URL` (default: current URL) - the URL for colyseus monitor to monitor (example: `server.game.com`).
|
|
60
|
+
|
|
57
61
|
## License
|
|
58
62
|
|
|
59
63
|
MIT
|
package/lib/api.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { MonitorOptions } from ".";
|
|
2
|
-
export declare function getAPI(opts: Partial<MonitorOptions>): import("express-serve-static-core").Router;
|
|
1
|
+
import { MonitorOptions } from ".";
|
|
2
|
+
export declare function getAPI(opts: Partial<MonitorOptions>): import("express-serve-static-core").Router;
|
package/lib/api.js
CHANGED
|
@@ -1,87 +1,88 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
-
});
|
|
10
|
-
};
|
|
11
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
-
};
|
|
14
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
-
|
|
16
|
-
const
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
'
|
|
22
|
-
'
|
|
23
|
-
'
|
|
24
|
-
'
|
|
25
|
-
'
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
api
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
data.
|
|
41
|
-
data.
|
|
42
|
-
data.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
res.
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
res.
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
res.
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
12
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
13
|
+
};
|
|
14
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
15
|
+
exports.getAPI = void 0;
|
|
16
|
+
const colyseus_1 = require("colyseus");
|
|
17
|
+
const express_1 = __importDefault(require("express"));
|
|
18
|
+
const node_os_utils_1 = __importDefault(require("node-os-utils"));
|
|
19
|
+
const UNAVAILABLE_ROOM_ERROR = "@colyseus/monitor: room $roomId is not available anymore.";
|
|
20
|
+
const DEFAULT_COLUMNS = [
|
|
21
|
+
'roomId',
|
|
22
|
+
'name',
|
|
23
|
+
'clients',
|
|
24
|
+
'maxClients',
|
|
25
|
+
'locked',
|
|
26
|
+
'elapsedTime',
|
|
27
|
+
];
|
|
28
|
+
function getAPI(opts) {
|
|
29
|
+
const api = express_1.default.Router();
|
|
30
|
+
api.get("/", (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
31
|
+
try {
|
|
32
|
+
const rooms = yield colyseus_1.matchMaker.query({});
|
|
33
|
+
let connections = 0;
|
|
34
|
+
res.json({
|
|
35
|
+
columns: opts.columns || DEFAULT_COLUMNS,
|
|
36
|
+
rooms: rooms.map(room => {
|
|
37
|
+
const data = room.toJSON();
|
|
38
|
+
connections += room.clients;
|
|
39
|
+
// additional data
|
|
40
|
+
data.locked = room.locked || false;
|
|
41
|
+
data.private = room.private;
|
|
42
|
+
data.maxClients = `${room.maxClients}`;
|
|
43
|
+
data.elapsedTime = Date.now() - new Date(room.createdAt).getTime();
|
|
44
|
+
return data;
|
|
45
|
+
}),
|
|
46
|
+
connections,
|
|
47
|
+
cpu: yield node_os_utils_1.default.cpu.usage(),
|
|
48
|
+
memory: yield node_os_utils_1.default.mem.used()
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
catch (e) {
|
|
52
|
+
const message = e.message;
|
|
53
|
+
console.error(message);
|
|
54
|
+
res.status(500);
|
|
55
|
+
res.json({ message });
|
|
56
|
+
}
|
|
57
|
+
}));
|
|
58
|
+
api.get("/room", (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
59
|
+
const roomId = req.query.roomId;
|
|
60
|
+
try {
|
|
61
|
+
const inspectData = yield colyseus_1.matchMaker.remoteRoomCall(roomId, "getInspectData");
|
|
62
|
+
res.json(inspectData);
|
|
63
|
+
}
|
|
64
|
+
catch (e) {
|
|
65
|
+
const message = UNAVAILABLE_ROOM_ERROR.replace("$roomId", roomId);
|
|
66
|
+
console.error(message);
|
|
67
|
+
res.status(500);
|
|
68
|
+
res.json({ message });
|
|
69
|
+
}
|
|
70
|
+
}));
|
|
71
|
+
api.get("/room/call", (req, res) => __awaiter(this, void 0, void 0, function* () {
|
|
72
|
+
const roomId = req.query.roomId;
|
|
73
|
+
const method = req.query.method;
|
|
74
|
+
const args = JSON.parse(req.query.args);
|
|
75
|
+
try {
|
|
76
|
+
const data = yield colyseus_1.matchMaker.remoteRoomCall(roomId, method, args);
|
|
77
|
+
res.json(data);
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
const message = UNAVAILABLE_ROOM_ERROR.replace("$roomId", roomId);
|
|
81
|
+
console.error(message);
|
|
82
|
+
res.status(500);
|
|
83
|
+
res.json({ message });
|
|
84
|
+
}
|
|
85
|
+
}));
|
|
86
|
+
return api;
|
|
87
|
+
}
|
|
88
|
+
exports.getAPI = getAPI;
|
package/lib/ext/Room.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {};
|
|
1
|
+
export {};
|
package/lib/ext/Room.js
CHANGED
|
@@ -1,69 +1,69 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
-
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
-
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
-
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
-
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
-
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
-
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
-
});
|
|
10
|
-
};
|
|
11
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
-
//
|
|
13
|
-
// Monkey-patch Colyseus' default behaviour
|
|
14
|
-
//
|
|
15
|
-
const colyseus_1 = require("colyseus");
|
|
16
|
-
function getStateSize(room) {
|
|
17
|
-
// TODO: `Serializer<T>` should provide a method for this (e.g. `serializer.hasState()`)
|
|
18
|
-
const hasState = (room._serializer.state || room._serializer.previousState);
|
|
19
|
-
const fullState = hasState && room._serializer.getFullState();
|
|
20
|
-
return fullState && (fullState.byteLength || fullState.length) || 0;
|
|
21
|
-
}
|
|
22
|
-
colyseus_1.Room.prototype.getAvailableData = function () {
|
|
23
|
-
return {
|
|
24
|
-
clients: this.clients.length,
|
|
25
|
-
maxClients: this.maxClients,
|
|
26
|
-
metadata: this.metadata,
|
|
27
|
-
roomId: this.roomId,
|
|
28
|
-
};
|
|
29
|
-
};
|
|
30
|
-
colyseus_1.Room.prototype.getRoomListData = function () {
|
|
31
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
32
|
-
const stateSize = getStateSize(this);
|
|
33
|
-
const elapsedTime = this.clock.elapsedTime;
|
|
34
|
-
const locked = this.locked;
|
|
35
|
-
const data = this.getAvailableData();
|
|
36
|
-
return Object.assign(Object.assign({}, data), { locked, elapsedTime, stateSize });
|
|
37
|
-
});
|
|
38
|
-
};
|
|
39
|
-
colyseus_1.Room.prototype.getInspectData = function () {
|
|
40
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
41
|
-
const state = this.state;
|
|
42
|
-
const stateSize = getStateSize(this);
|
|
43
|
-
const data = this.getAvailableData();
|
|
44
|
-
const clients = this.clients.map((client) => ({ sessionId: client.sessionId }));
|
|
45
|
-
const locked = this.locked;
|
|
46
|
-
return Object.assign(Object.assign({}, data), { locked, clients, state, stateSize });
|
|
47
|
-
});
|
|
48
|
-
};
|
|
49
|
-
// Actions
|
|
50
|
-
colyseus_1.Room.prototype._forceClientDisconnect = function (sessionId) {
|
|
51
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
52
|
-
for (let i = 0; i < this.clients.length; i++) {
|
|
53
|
-
if (this.clients[i].sessionId === sessionId) {
|
|
54
|
-
this.clients[i].terminate();
|
|
55
|
-
break;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
});
|
|
59
|
-
};
|
|
60
|
-
colyseus_1.Room.prototype._sendMessageToClient = function (sessionId, data) {
|
|
61
|
-
return __awaiter(this, void 0, void 0, function* () {
|
|
62
|
-
for (let i = 0; i < this.clients.length; i++) {
|
|
63
|
-
if (this.clients[i].sessionId === sessionId) {
|
|
64
|
-
this.send(this.clients[i], data);
|
|
65
|
-
break;
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
});
|
|
69
|
-
};
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
//
|
|
13
|
+
// Monkey-patch Colyseus' default behaviour
|
|
14
|
+
//
|
|
15
|
+
const colyseus_1 = require("colyseus");
|
|
16
|
+
function getStateSize(room) {
|
|
17
|
+
// TODO: `Serializer<T>` should provide a method for this (e.g. `serializer.hasState()`)
|
|
18
|
+
const hasState = (room._serializer.state || room._serializer.previousState);
|
|
19
|
+
const fullState = hasState && room._serializer.getFullState();
|
|
20
|
+
return fullState && (fullState.byteLength || fullState.length) || 0;
|
|
21
|
+
}
|
|
22
|
+
colyseus_1.Room.prototype.getAvailableData = function () {
|
|
23
|
+
return {
|
|
24
|
+
clients: this.clients.length,
|
|
25
|
+
maxClients: this.maxClients,
|
|
26
|
+
metadata: this.metadata,
|
|
27
|
+
roomId: this.roomId,
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
colyseus_1.Room.prototype.getRoomListData = function () {
|
|
31
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
32
|
+
const stateSize = getStateSize(this);
|
|
33
|
+
const elapsedTime = this.clock.elapsedTime;
|
|
34
|
+
const locked = this.locked;
|
|
35
|
+
const data = this.getAvailableData();
|
|
36
|
+
return Object.assign(Object.assign({}, data), { locked, elapsedTime, stateSize });
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
colyseus_1.Room.prototype.getInspectData = function () {
|
|
40
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
41
|
+
const state = this.state;
|
|
42
|
+
const stateSize = getStateSize(this);
|
|
43
|
+
const data = this.getAvailableData();
|
|
44
|
+
const clients = this.clients.map((client) => ({ sessionId: client.sessionId }));
|
|
45
|
+
const locked = this.locked;
|
|
46
|
+
return Object.assign(Object.assign({}, data), { locked, clients, state, stateSize });
|
|
47
|
+
});
|
|
48
|
+
};
|
|
49
|
+
// Actions
|
|
50
|
+
colyseus_1.Room.prototype._forceClientDisconnect = function (sessionId) {
|
|
51
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
52
|
+
for (let i = 0; i < this.clients.length; i++) {
|
|
53
|
+
if (this.clients[i].sessionId === sessionId) {
|
|
54
|
+
this.clients[i].terminate();
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
colyseus_1.Room.prototype._sendMessageToClient = function (sessionId, data) {
|
|
61
|
+
return __awaiter(this, void 0, void 0, function* () {
|
|
62
|
+
for (let i = 0; i < this.clients.length; i++) {
|
|
63
|
+
if (this.clients[i].sessionId === sessionId) {
|
|
64
|
+
this.send(this.clients[i], data);
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
};
|
package/lib/index.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import express from 'express';
|
|
2
|
-
import './ext/Room';
|
|
3
|
-
export interface MonitorOptions {
|
|
4
|
-
columns: Array<'roomId' | 'name' | 'clients' | 'maxClients' | 'locked' | 'elapsedTime' | {
|
|
5
|
-
metadata: string;
|
|
6
|
-
}>;
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* TODO: expose the `router` instead on next major version.
|
|
10
|
-
*/
|
|
11
|
-
export declare function monitor(opts?: Partial<MonitorOptions>): express.Router;
|
|
1
|
+
import express from 'express';
|
|
2
|
+
import './ext/Room';
|
|
3
|
+
export interface MonitorOptions {
|
|
4
|
+
columns: Array<'roomId' | 'name' | 'clients' | 'maxClients' | 'locked' | 'elapsedTime' | {
|
|
5
|
+
metadata: string;
|
|
6
|
+
}>;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* TODO: expose the `router` instead on next major version.
|
|
10
|
+
*/
|
|
11
|
+
export declare function monitor(opts?: Partial<MonitorOptions>): express.Router;
|
package/lib/index.js
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
const
|
|
9
|
-
require("./
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
router
|
|
17
|
-
router.use('/
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.monitor = void 0;
|
|
7
|
+
const express_1 = __importDefault(require("express"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const api_1 = require("./api");
|
|
10
|
+
require("./ext/Room");
|
|
11
|
+
const frontendDirectory = path_1.default.resolve(__dirname, "..", "lib", "static");
|
|
12
|
+
/**
|
|
13
|
+
* TODO: expose the `router` instead on next major version.
|
|
14
|
+
*/
|
|
15
|
+
function monitor(opts = {}) {
|
|
16
|
+
const router = express_1.default.Router();
|
|
17
|
+
router.use('/', express_1.default.static(frontendDirectory));
|
|
18
|
+
router.use('/api', api_1.getAPI(opts));
|
|
19
|
+
return router;
|
|
20
|
+
}
|
|
21
|
+
exports.monitor = monitor;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colyseus/monitor",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.3",
|
|
4
4
|
"description": "Web Monitoring Panel for Colyseus",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"types": "lib/index.d.ts",
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
"@types/react": "^16.3.9",
|
|
36
36
|
"@types/react-router-dom": "^4.2.6",
|
|
37
37
|
"@types/superagent": "^3.5.8",
|
|
38
|
-
"colyseus": "^0.
|
|
38
|
+
"colyseus": "^0.12.0",
|
|
39
39
|
"cors": "^2.8.4",
|
|
40
40
|
"css-loader": "^0.28.11",
|
|
41
41
|
"extract-text-webpack-plugin": "^4.0.0-beta.0",
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
"superagent": "^3.8.2",
|
|
53
53
|
"ts-loader": "^4.2.0",
|
|
54
54
|
"ts-node": "^5.0.1",
|
|
55
|
-
"typescript": "^3.
|
|
55
|
+
"typescript": "^3.9.7",
|
|
56
56
|
"webpack": "^4.5.0",
|
|
57
57
|
"webpack-cli": "^3.2.3",
|
|
58
58
|
"webpack-dev-middleware": "^3.1.2",
|
|
@@ -61,8 +61,5 @@
|
|
|
61
61
|
"dependencies": {
|
|
62
62
|
"express": "^4.16.3",
|
|
63
63
|
"node-os-utils": "^1.2.0"
|
|
64
|
-
},
|
|
65
|
-
"peerDependencies": {
|
|
66
|
-
"colyseus": "^0.11.0"
|
|
67
64
|
}
|
|
68
65
|
}
|