2019-09-17 14:03:16 +02:00
|
|
|
import { Game } from "planetwars";
|
2020-03-29 06:21:24 +02:00
|
|
|
import { memory } from "planetwars/planetwars_bg";
|
2019-09-19 17:52:48 +02:00
|
|
|
import { Resizer, resizeCanvasToDisplaySize, FPSCounter, url_to_mesh, Mesh } from "./webgl/util";
|
|
|
|
import { Shader, Uniform4f, Uniform2fv, Uniform3fv, Uniform1i, Uniform1f, Uniform2f, ShaderFactory, Uniform3f, UniformMatrix3fv } from './webgl/shader';
|
2019-09-17 20:19:04 +02:00
|
|
|
import { Renderer } from "./webgl/renderer";
|
|
|
|
import { VertexBuffer, IndexBuffer } from "./webgl/buffer";
|
|
|
|
import { VertexBufferLayout, VertexArray } from "./webgl/vertexBufferLayout";
|
|
|
|
import { callbackify } from "util";
|
2019-09-17 18:27:44 +02:00
|
|
|
|
2019-09-18 12:29:56 +02:00
|
|
|
function f32v(ptr: number, size: number): Float32Array {
|
|
|
|
return new Float32Array(memory.buffer, ptr, size);
|
|
|
|
}
|
|
|
|
|
|
|
|
function i32v(ptr: number, size: number): Int32Array {
|
|
|
|
return new Int32Array(memory.buffer, ptr, size);
|
|
|
|
}
|
|
|
|
|
2019-09-24 15:38:04 +02:00
|
|
|
export function set_game_name(name: string) {
|
|
|
|
GAMENAME.innerHTML = name;
|
|
|
|
}
|
|
|
|
|
|
|
|
const GAMENAME = document.getElementById("name");
|
|
|
|
|
2019-09-21 17:06:24 +02:00
|
|
|
const TURNCOUNTER = document.getElementById("turnCounter");
|
|
|
|
|
2019-09-17 20:19:04 +02:00
|
|
|
const COUNTER = new FPSCounter();
|
2019-09-21 16:54:48 +02:00
|
|
|
const LOADER = document.getElementById("main");
|
2019-09-17 20:19:04 +02:00
|
|
|
|
2019-09-21 10:43:03 +02:00
|
|
|
const SLIDER = <HTMLInputElement>document.getElementById("turnSlider");
|
2019-09-21 11:20:07 +02:00
|
|
|
const FILESELECTOR = <HTMLInputElement> document.getElementById("fileselect");
|
2019-09-21 11:30:00 +02:00
|
|
|
const SPEED = <HTMLInputElement> document.getElementById("speed");
|
2019-09-21 16:54:48 +02:00
|
|
|
|
|
|
|
document.getElementById("addbutton").onclick = function() {
|
|
|
|
FILESELECTOR.click();
|
|
|
|
}
|
|
|
|
|
2019-09-21 20:13:57 +02:00
|
|
|
export function set_loading(loading: boolean) {
|
2019-09-17 20:19:04 +02:00
|
|
|
if (loading) {
|
|
|
|
if (!LOADER.classList.contains("loading")) {
|
|
|
|
LOADER.classList.add("loading");
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
LOADER.classList.remove("loading");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const URL = window.location.origin+window.location.pathname;
|
2019-09-21 18:56:46 +02:00
|
|
|
export const LOCATION = URL.substring(0, URL.lastIndexOf("/") + 1);
|
2019-09-17 18:27:44 +02:00
|
|
|
const CANVAS = <HTMLCanvasElement>document.getElementById("c");
|
2019-09-17 20:19:04 +02:00
|
|
|
const RESOLUTION = [CANVAS.width, CANVAS.height];
|
|
|
|
|
|
|
|
const GL = CANVAS.getContext("webgl");
|
2019-09-19 17:52:48 +02:00
|
|
|
|
2019-09-25 11:06:33 +02:00
|
|
|
var ms_per_frame = parseInt(SPEED.value);
|
2019-09-21 11:30:00 +02:00
|
|
|
|
2019-09-22 12:01:24 +02:00
|
|
|
resizeCanvasToDisplaySize(CANVAS);
|
2019-09-17 20:19:04 +02:00
|
|
|
|
|
|
|
GL.clearColor(0, 0, 0, 0);
|
|
|
|
GL.clear(GL.COLOR_BUFFER_BIT);
|
|
|
|
|
|
|
|
GL.enable(GL.BLEND);
|
|
|
|
GL.blendFunc(GL.SRC_ALPHA, GL.ONE_MINUS_SRC_ALPHA);
|
|
|
|
|
|
|
|
var SHADERFACOTRY: ShaderFactory;
|
|
|
|
ShaderFactory.create_factory(
|
|
|
|
LOCATION + "static/shaders/frag/simple.glsl", LOCATION + "static/shaders/vert/simple.glsl"
|
|
|
|
).then((e) => SHADERFACOTRY = e);
|
2019-09-17 18:27:44 +02:00
|
|
|
|
2019-09-24 14:29:13 +02:00
|
|
|
var VOR_SHADER_FACTORY: ShaderFactory;
|
|
|
|
ShaderFactory.create_factory(
|
|
|
|
LOCATION + "static/shaders/frag/vor.glsl", LOCATION + "static/shaders/vert/vor.glsl"
|
|
|
|
).then((e) => VOR_SHADER_FACTORY = e);
|
|
|
|
|
2019-09-17 20:19:04 +02:00
|
|
|
class GameInstance {
|
|
|
|
resizer: Resizer;
|
|
|
|
game: Game;
|
|
|
|
shader: Shader;
|
2019-09-24 14:29:13 +02:00
|
|
|
vor_shader: Shader;
|
2019-09-17 20:19:04 +02:00
|
|
|
renderer: Renderer;
|
2019-09-18 12:29:56 +02:00
|
|
|
planet_count: number;
|
|
|
|
|
2019-09-21 10:17:35 +02:00
|
|
|
playing = true; // 0 is paused, 1 is playing but not rerendered, 2 is playing and rerendered
|
|
|
|
time_stopped_delta = 0;
|
2019-09-18 12:29:56 +02:00
|
|
|
last_time = 0;
|
|
|
|
frame = -1;
|
2019-09-17 20:19:04 +02:00
|
|
|
|
2019-09-20 17:48:00 +02:00
|
|
|
ship_indices: number[];
|
|
|
|
|
2019-09-21 17:06:24 +02:00
|
|
|
turn_count = 0;
|
|
|
|
|
2019-09-20 17:48:00 +02:00
|
|
|
constructor(game: Game, meshes: Mesh[], ship_mesh: Mesh) {
|
2019-09-17 20:19:04 +02:00
|
|
|
this.game = game;
|
2019-09-18 12:29:56 +02:00
|
|
|
this.planet_count = this.game.get_planet_count();
|
|
|
|
this.shader = SHADERFACOTRY.create_shader(GL, {"MAX_CIRCLES": ''+this.planet_count});
|
2019-09-24 14:29:13 +02:00
|
|
|
this.vor_shader = VOR_SHADER_FACTORY.create_shader(GL, {"PLANETS": ''+this.planet_count});
|
2019-09-18 12:29:56 +02:00
|
|
|
this.resizer = new Resizer(CANVAS, [...f32v(game.get_viewbox(), 4)], true);
|
2019-09-17 20:19:04 +02:00
|
|
|
this.renderer = new Renderer();
|
2019-09-19 18:07:46 +02:00
|
|
|
this.game.update_turn(0);
|
2019-09-19 17:52:48 +02:00
|
|
|
|
2019-09-24 14:29:13 +02:00
|
|
|
const indexBuffer = new IndexBuffer(GL, [
|
|
|
|
0, 1, 2,
|
|
|
|
1, 2, 3,
|
|
|
|
]);
|
|
|
|
|
|
|
|
const positionBuffer = new VertexBuffer(GL, [
|
|
|
|
-1, -1,
|
|
|
|
-1, 1,
|
|
|
|
1, -1,
|
|
|
|
1, 1,
|
|
|
|
]);
|
|
|
|
|
|
|
|
const layout = new VertexBufferLayout();
|
|
|
|
layout.push(GL.FLOAT, 2, 4, "a_pos");
|
|
|
|
|
|
|
|
const vao = new VertexArray();
|
|
|
|
vao.addBuffer(positionBuffer, layout);
|
|
|
|
|
|
|
|
this.renderer.addToDraw(indexBuffer, vao, this.vor_shader);
|
|
|
|
|
2019-09-21 10:17:35 +02:00
|
|
|
// Setup key handling
|
|
|
|
document.addEventListener('keydown', this.handleKey.bind(this));
|
|
|
|
|
2019-09-19 17:52:48 +02:00
|
|
|
const planets = f32v(game.get_planets(), this.planet_count * 3);
|
|
|
|
|
|
|
|
for(let i=0; i < this.planet_count; i++){
|
|
|
|
|
|
|
|
const transform = new UniformMatrix3fv([
|
|
|
|
1, 0, 0,
|
|
|
|
0, 1, 0,
|
2019-09-20 19:31:32 +02:00
|
|
|
-planets[i*3], -planets[i*3+1], 1,
|
2019-09-19 17:52:48 +02:00
|
|
|
]);
|
|
|
|
|
|
|
|
const indexBuffer = new IndexBuffer(GL, meshes[i % meshes.length].cells);
|
|
|
|
const positionBuffer = new VertexBuffer(GL, meshes[i % meshes.length].positions);
|
|
|
|
|
|
|
|
const layout = new VertexBufferLayout();
|
|
|
|
layout.push(GL.FLOAT, 3, 4, "a_position");
|
|
|
|
const vao = new VertexArray();
|
|
|
|
vao.addBuffer(positionBuffer, layout);
|
|
|
|
|
|
|
|
this.renderer.addToDraw(
|
|
|
|
indexBuffer,
|
|
|
|
vao,
|
|
|
|
this.shader,
|
|
|
|
{
|
|
|
|
"u_trans": transform,
|
2019-09-20 17:48:00 +02:00
|
|
|
"u_trans_next": transform,
|
2019-09-19 17:52:48 +02:00
|
|
|
}
|
2019-09-20 17:48:00 +02:00
|
|
|
);
|
2019-09-19 17:52:48 +02:00
|
|
|
}
|
2019-09-18 12:29:56 +02:00
|
|
|
|
2019-09-21 17:06:24 +02:00
|
|
|
this.turn_count = game.turn_count();
|
|
|
|
|
2019-09-20 17:48:00 +02:00
|
|
|
this.ship_indices = [];
|
|
|
|
const ship_ibo = new IndexBuffer(GL, ship_mesh.cells);
|
|
|
|
const ship_positions = new VertexBuffer(GL, ship_mesh.positions);
|
|
|
|
const ship_layout = new VertexBufferLayout();
|
|
|
|
ship_layout.push(GL.FLOAT, 3, 4, "a_position");
|
|
|
|
const ship_vao = new VertexArray();
|
|
|
|
ship_vao.addBuffer(ship_positions, ship_layout);
|
|
|
|
|
|
|
|
for (let i = 0; i < this.game.get_max_ships(); i++) {
|
|
|
|
this.ship_indices.push(
|
|
|
|
this.renderer.addToDraw(
|
|
|
|
ship_ibo,
|
|
|
|
ship_vao,
|
|
|
|
this.shader,
|
|
|
|
{}
|
|
|
|
)
|
|
|
|
);
|
|
|
|
}
|
2019-09-21 10:43:03 +02:00
|
|
|
|
2019-09-24 14:29:13 +02:00
|
|
|
this.vor_shader.uniform(GL, "u_planets", new Uniform3fv(planets));
|
|
|
|
|
2019-09-21 10:43:03 +02:00
|
|
|
// Set slider correctly
|
2019-09-21 17:06:24 +02:00
|
|
|
SLIDER.max = this.turn_count - 1 + '';
|
2019-09-17 20:19:04 +02:00
|
|
|
}
|
|
|
|
|
2019-09-21 20:19:31 +02:00
|
|
|
on_resize() {
|
|
|
|
this.resizer = new Resizer(CANVAS, [...f32v(this.game.get_viewbox(), 4)], true);
|
|
|
|
}
|
|
|
|
|
2019-09-21 10:32:57 +02:00
|
|
|
_update_state() {
|
|
|
|
const colours = f32v(this.game.get_planet_colors(), this.planet_count * 6);
|
2019-09-24 14:29:13 +02:00
|
|
|
|
|
|
|
this.vor_shader.uniform(GL, "u_planet_colours", new Uniform3fv(colours));
|
|
|
|
|
2019-09-21 10:32:57 +02:00
|
|
|
for(let i=0; i < this.planet_count; i++){
|
|
|
|
const u = new Uniform3f(colours[i*6], colours[i*6 + 1], colours[i*6 + 2]);
|
2019-09-24 14:29:13 +02:00
|
|
|
this.renderer.updateUniform(i+1, (us) => us["u_color"] = u);
|
2019-09-21 10:32:57 +02:00
|
|
|
const u2 = new Uniform3f(colours[i*6 + 3], colours[i*6 + 4], colours[i*6 + 5]);
|
2019-09-24 14:29:13 +02:00
|
|
|
this.renderer.updateUniform(i+1, (us) => us["u_color_next"] = u2);
|
2019-09-21 10:32:57 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
const ships = f32v(this.game.get_ship_locations(), this.game.get_ship_count() * 9 * 2);
|
|
|
|
const ship_colours = f32v(this.game.get_ship_colours(), this.game.get_ship_count() * 3);
|
|
|
|
|
|
|
|
for (let i=0; i < this.game.get_max_ships(); i++) {
|
|
|
|
const index = this.ship_indices[i];
|
|
|
|
if (i < this.game.get_ship_count()) {
|
|
|
|
|
|
|
|
this.renderer.enableRendershit(index);
|
|
|
|
|
|
|
|
const u = new Uniform3f(ship_colours[i*3], ship_colours[i*3 + 1], ship_colours[i*3 + 2]);
|
|
|
|
// const t1 = new UniformMatrix3fv(new Float32Array(ships, i * 18, 9));
|
|
|
|
// const t2 = new UniformMatrix3fv(new Float32Array(ships, i * 18 + 9, 9));
|
|
|
|
|
|
|
|
const t1 = new UniformMatrix3fv(ships.slice(i * 18, i * 18 + 9));
|
|
|
|
const t2 = new UniformMatrix3fv(ships.slice(i * 18 + 9, i * 18 + 18));
|
|
|
|
|
|
|
|
this.renderer.updateUniform(index, (us) => {
|
|
|
|
us["u_color"] = u;
|
|
|
|
us["u_color_next"] = u;
|
|
|
|
us["u_trans"] = t1;
|
|
|
|
us["u_trans_next"] = t2;
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
this.renderer.disableRenderShift(index);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-17 20:19:04 +02:00
|
|
|
render(time: number) {
|
2019-09-21 10:17:35 +02:00
|
|
|
COUNTER.frame(time);
|
|
|
|
|
|
|
|
if (!this.playing) {
|
|
|
|
this.last_time = time;
|
2019-09-21 11:31:28 +02:00
|
|
|
|
|
|
|
this.shader.uniform(GL, "u_viewbox", new Uniform4f(this.resizer.get_viewbox()));
|
2019-09-24 14:29:13 +02:00
|
|
|
this.vor_shader.uniform(GL, "u_viewbox", new Uniform4f(this.resizer.get_viewbox()));
|
|
|
|
|
2019-09-21 11:31:28 +02:00
|
|
|
this.renderer.render(GL);
|
2019-09-21 10:17:35 +02:00
|
|
|
return;
|
|
|
|
}
|
2019-09-21 11:30:00 +02:00
|
|
|
if (time > this.last_time + ms_per_frame) {
|
2019-09-20 17:48:00 +02:00
|
|
|
|
2019-09-18 12:29:56 +02:00
|
|
|
this.last_time = time;
|
2019-09-21 10:32:57 +02:00
|
|
|
this.updateTurn(this.frame + 1);
|
2019-09-19 17:52:48 +02:00
|
|
|
}
|
2019-09-20 17:48:00 +02:00
|
|
|
|
2019-09-19 17:52:48 +02:00
|
|
|
GL.bindFramebuffer(GL.FRAMEBUFFER, null);
|
|
|
|
GL.viewport(0, 0, GL.canvas.width, GL.canvas.height);
|
|
|
|
GL.clear(GL.COLOR_BUFFER_BIT | GL.DEPTH_BUFFER_BIT);
|
2019-09-17 20:19:04 +02:00
|
|
|
|
2019-09-24 14:29:13 +02:00
|
|
|
this.vor_shader.uniform(GL, "u_viewbox", new Uniform4f(this.resizer.get_viewbox()));
|
|
|
|
this.vor_shader.uniform(GL, "u_resolution", new Uniform2f(RESOLUTION));
|
|
|
|
|
2019-09-21 11:30:00 +02:00
|
|
|
this.shader.uniform(GL, "u_time", new Uniform1f((time - this.last_time) / ms_per_frame));
|
2019-09-17 20:19:04 +02:00
|
|
|
this.shader.uniform(GL, "u_mouse", new Uniform2f(this.resizer.get_mouse_pos()));
|
|
|
|
this.shader.uniform(GL, "u_viewbox", new Uniform4f(this.resizer.get_viewbox()));
|
|
|
|
this.shader.uniform(GL, "u_resolution", new Uniform2f(RESOLUTION));
|
|
|
|
|
|
|
|
this.renderer.render(GL);
|
2019-09-21 10:17:35 +02:00
|
|
|
}
|
2019-09-19 17:52:48 +02:00
|
|
|
|
2019-09-21 10:32:57 +02:00
|
|
|
updateTurn(turn: number) {
|
|
|
|
this.frame = Math.max(0, turn);
|
|
|
|
const new_frame = this.game.update_turn(this.frame);
|
|
|
|
if (new_frame < this.frame) {
|
|
|
|
this.frame = new_frame;
|
|
|
|
this.playing = false;
|
|
|
|
} else {
|
|
|
|
this._update_state();
|
2019-09-21 10:43:03 +02:00
|
|
|
this.playing = true;
|
2019-09-21 10:32:57 +02:00
|
|
|
}
|
2019-09-21 10:43:03 +02:00
|
|
|
|
2019-09-21 17:06:24 +02:00
|
|
|
TURNCOUNTER.innerHTML = this.frame + " / " + this.turn_count;
|
2019-09-21 10:43:03 +02:00
|
|
|
SLIDER.value = this.frame + '';
|
2019-09-21 10:32:57 +02:00
|
|
|
}
|
|
|
|
|
2019-09-21 10:17:35 +02:00
|
|
|
handleKey(event: KeyboardEvent) {
|
|
|
|
// Space
|
|
|
|
if (event.keyCode == 32) {
|
|
|
|
if (this.playing) {
|
|
|
|
this.playing = false;
|
|
|
|
} else {
|
|
|
|
this.playing = true;
|
|
|
|
}
|
2019-09-21 10:32:57 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Arrow left
|
|
|
|
if (event.keyCode == 37) {
|
|
|
|
// This feels more natural than -1 what it should be, I think
|
|
|
|
this.updateTurn(this.frame - 2);
|
|
|
|
}
|
2019-09-21 10:17:35 +02:00
|
|
|
|
2019-09-21 10:32:57 +02:00
|
|
|
// Arrow right
|
|
|
|
if (event.keyCode == 39) {
|
|
|
|
this.updateTurn(this.frame + 1);
|
2019-09-21 10:17:35 +02:00
|
|
|
}
|
2019-09-21 11:30:00 +02:00
|
|
|
|
|
|
|
// d key
|
|
|
|
if (event.keyCode == 68) {
|
|
|
|
SPEED.value = ms_per_frame + 10 + '';
|
|
|
|
SPEED.onchange(undefined);
|
|
|
|
}
|
|
|
|
|
|
|
|
// a key
|
|
|
|
if (event.keyCode == 65) {
|
|
|
|
SPEED.value = Math.max(ms_per_frame - 10, 0) + '';
|
|
|
|
SPEED.onchange(undefined);
|
|
|
|
}
|
2019-09-17 20:19:04 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
var game_instance: GameInstance;
|
2019-09-21 11:37:26 +02:00
|
|
|
var meshes;
|
|
|
|
|
|
|
|
export async function set_instance(source: string) {
|
|
|
|
if (!meshes) {
|
|
|
|
meshes = await Promise.all(
|
|
|
|
["ship.svg", "earth.svg", "mars.svg", "venus.svg"].map(
|
|
|
|
(name) => "static/res/assets/" + name
|
|
|
|
).map(url_to_mesh)
|
|
|
|
);
|
|
|
|
}
|
2019-09-21 16:54:48 +02:00
|
|
|
|
2019-09-22 12:01:24 +02:00
|
|
|
resizeCanvasToDisplaySize(CANVAS);
|
|
|
|
|
2019-09-21 11:37:26 +02:00
|
|
|
game_instance = new GameInstance(Game.new(source), meshes.slice(1), meshes[0]);
|
2019-09-21 20:13:57 +02:00
|
|
|
|
|
|
|
set_loading(false);
|
2019-09-17 14:03:16 +02:00
|
|
|
}
|
2019-09-17 20:19:04 +02:00
|
|
|
|
2019-09-21 20:19:31 +02:00
|
|
|
window.addEventListener('resize', function() {
|
2019-09-22 12:01:24 +02:00
|
|
|
resizeCanvasToDisplaySize(CANVAS);
|
2019-09-21 20:19:31 +02:00
|
|
|
|
|
|
|
if (game_instance) {
|
|
|
|
game_instance.on_resize();
|
|
|
|
}
|
|
|
|
}, { capture: false, passive: true})
|
|
|
|
|
2019-09-21 10:43:03 +02:00
|
|
|
SLIDER.oninput = function() {
|
|
|
|
if (game_instance) {
|
|
|
|
game_instance.updateTurn(parseInt(SLIDER.value));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-09-21 11:20:07 +02:00
|
|
|
FILESELECTOR.onchange = function(){
|
|
|
|
const file = FILESELECTOR.files[0];
|
2019-09-21 16:54:48 +02:00
|
|
|
if(!file) { return; }
|
2019-09-21 11:20:07 +02:00
|
|
|
var reader = new FileReader();
|
|
|
|
|
|
|
|
reader.onload = function() {
|
2019-09-21 11:37:26 +02:00
|
|
|
set_instance(<string> reader.result);
|
2019-09-21 11:20:07 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
reader.readAsText(file);
|
|
|
|
}
|
2019-09-17 20:19:04 +02:00
|
|
|
|
2019-09-21 11:30:00 +02:00
|
|
|
SPEED.onchange = function() {
|
|
|
|
ms_per_frame = parseInt(SPEED.value);
|
|
|
|
}
|
|
|
|
|
2019-09-17 20:19:04 +02:00
|
|
|
function step(time: number) {
|
|
|
|
if (game_instance) {
|
2019-09-20 17:48:00 +02:00
|
|
|
game_instance.render(time);
|
2019-09-17 20:19:04 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
requestAnimationFrame(step);
|
|
|
|
}
|
|
|
|
set_loading(true);
|
|
|
|
|
2020-03-27 15:26:04 +01:00
|
|
|
requestAnimationFrame(step);
|
2019-09-28 21:05:25 +02:00
|
|
|
|
|
|
|
|
2020-03-27 15:26:04 +01:00
|
|
|
// import { voronoi, Point } from './voronoi'
|
|
|
|
// function test() {
|
|
|
|
// const points = [
|
|
|
|
// new Point(14, 6),
|
|
|
|
// new Point(13, 11),
|
|
|
|
// new Point(8, 7.5),
|
|
|
|
// new Point(7, 4),
|
|
|
|
// new Point(4, 11),
|
|
|
|
// ];
|
2019-09-28 21:05:25 +02:00
|
|
|
|
2020-03-27 15:26:04 +01:00
|
|
|
// console.log(voronoi(points));
|
|
|
|
// }
|
2019-09-28 21:05:25 +02:00
|
|
|
|
2020-03-27 15:26:04 +01:00
|
|
|
// import { test as dcelt_test } from './dcel';
|
|
|
|
// // dcelt_test();
|
2019-09-29 21:56:04 +02:00
|
|
|
|
2020-03-27 15:26:04 +01:00
|
|
|
// test();
|