mirror of https://github.com/dirtbags/tanks.git
Merge branch 'master' of woozle.org:projects/ctf/tanks
This commit is contained in:
commit
f202312979
|
@ -39,7 +39,7 @@ def read_tank(infile):
|
||||||
def post_tank(headers, code, url):
|
def post_tank(headers, code, url):
|
||||||
token = os.environ.get('token')
|
token = os.environ.get('token')
|
||||||
if not token:
|
if not token:
|
||||||
raise RuntimeError("Must provide token in 'token' environment variable")
|
token = open('token').readline().strip()
|
||||||
request = {}
|
request = {}
|
||||||
request['token'] = token
|
request['token'] = token
|
||||||
request['name'] = headers.get('Tank-Name', '')
|
request['name'] = headers.get('Tank-Name', '')
|
||||||
|
|
|
@ -37,6 +37,10 @@
|
||||||
<script type="application/javascript">
|
<script type="application/javascript">
|
||||||
window.onload = function() { design(); update(); };
|
window.onload = function() { design(); update(); };
|
||||||
function onSubmit() {
|
function onSubmit() {
|
||||||
|
if ($('[name="name"]').val() === "") {
|
||||||
|
$('#submit-feedback').html("No name?");
|
||||||
|
return;
|
||||||
|
}
|
||||||
$('#submit-feedback').html("Submitting...");
|
$('#submit-feedback').html("Submitting...");
|
||||||
|
|
||||||
// http://stackoverflow.com/questions/169506/obtain-form-input-fields-using-jquery
|
// http://stackoverflow.com/questions/169506/obtain-form-input-fields-using-jquery
|
||||||
|
@ -78,19 +82,26 @@
|
||||||
$('[name="' + name + '"]').val(val);
|
$('[name="' + name + '"]').val(val);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
var finishedreqs = 0;
|
||||||
var request = makerequest("name");
|
var request = makerequest("name");
|
||||||
request.done(function(msg) {
|
request.done(function(msg) {
|
||||||
$('#submit-feedback').html("");
|
|
||||||
setval("name", msg);
|
setval("name", msg);
|
||||||
|
|
||||||
|
var gotreq = function() {
|
||||||
|
finishedreqs++;
|
||||||
|
if (finishedreqs == 13) {
|
||||||
|
$('#submit-feedback').html("Retrieved.");
|
||||||
|
}
|
||||||
|
};
|
||||||
request = makerequest("author");
|
request = makerequest("author");
|
||||||
request.done(function(msg) { setval("author", msg); } );
|
request.done(function(msg) { gotreq(); setval("author", msg); } );
|
||||||
request = makerequest("color");
|
request = makerequest("color");
|
||||||
request.done(function(msg) { setval("color", msg.replace(/[\r\n]/g, '')); update(); } );
|
request.done(function(msg) { gotreq(); setval("color", msg.replace(/[\r\n]/g, '')); update(); } );
|
||||||
request = makerequest("program");
|
request = makerequest("program");
|
||||||
request.done(function(msg) { setval("program", msg); } );
|
request.done(function(msg) { gotreq(); setval("program", msg); } );
|
||||||
var sensorfunc = function(id) {
|
var sensorfunc = function(id) {
|
||||||
return function(msg) {
|
return function(msg) {
|
||||||
|
gotreq();
|
||||||
var vals = msg.replace(/[\r\n]/g, '').split(" ");
|
var vals = msg.replace(/[\r\n]/g, '').split(" ");
|
||||||
setval("s"+id+"r", vals[0]);
|
setval("s"+id+"r", vals[0]);
|
||||||
setval("s"+id+"a", vals[1]);
|
setval("s"+id+"a", vals[1]);
|
||||||
|
|
78
jstanks.js
78
jstanks.js
|
@ -73,22 +73,20 @@ var SPACING = 150;
|
||||||
var MEMORY_SIZE = 10;
|
var MEMORY_SIZE = 10;
|
||||||
|
|
||||||
var Forf = function() {
|
var Forf = function() {
|
||||||
this.datastack = [];
|
|
||||||
this.cmdstack = [];
|
|
||||||
this.mem = new Object();
|
this.mem = new Object();
|
||||||
this.builtins = new Object();
|
this.builtins = new Object();
|
||||||
|
|
||||||
this.builtins["debug!"] = function(myforf) { document.getElementById('debug').innerHTML = myforf.datastack.pop(); };
|
this.builtins["debug!"] = function(myforf) { document.getElementById('debug').innerHTML = myforf.popData(); };
|
||||||
var unfunc = function(func) {
|
var unfunc = function(func) {
|
||||||
return function(myforf) {
|
return function(myforf) {
|
||||||
var a = myforf.datastack.pop();
|
var a = myforf.popData();
|
||||||
myforf.datastack.push(~~func(a)); // truncate, FIXME
|
myforf.datastack.push(~~func(a)); // truncate, FIXME
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
var binfunc = function(func) {
|
var binfunc = function(func) {
|
||||||
return function(myforf) {
|
return function(myforf) {
|
||||||
var a = myforf.datastack.pop();
|
var a = myforf.popData();
|
||||||
var b = myforf.datastack.pop();
|
var b = myforf.popData();
|
||||||
myforf.datastack.push(~~func(b,a)); // truncate?, FIXME
|
myforf.datastack.push(~~func(b,a)); // truncate?, FIXME
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
@ -119,22 +117,22 @@ var Forf = function() {
|
||||||
this.builtins["abs"] = unfunc(function(a) { return Math.abs(a); });
|
this.builtins["abs"] = unfunc(function(a) { return Math.abs(a); });
|
||||||
// FIXME: the three following functions can only manipulate numbers in cforf
|
// FIXME: the three following functions can only manipulate numbers in cforf
|
||||||
this.builtins["dup"] = function(myforf) {
|
this.builtins["dup"] = function(myforf) {
|
||||||
var val = myforf.datastack.pop();
|
var val = myforf.popData();
|
||||||
myforf.datastack.push(val);
|
myforf.datastack.push(val);
|
||||||
myforf.datastack.push(val);
|
myforf.datastack.push(val);
|
||||||
};
|
};
|
||||||
this.builtins["pop"] = function(myforf) {
|
this.builtins["pop"] = function(myforf) {
|
||||||
myforf.datastack.pop();
|
myforf.popData();
|
||||||
};
|
};
|
||||||
this.builtins["exch"] = function(myforf) {
|
this.builtins["exch"] = function(myforf) {
|
||||||
var a = myforf.datastack.pop();
|
var a = myforf.popData();
|
||||||
var b = myforf.datastack.pop();
|
var b = myforf.popData();
|
||||||
myforf.datastack.push(a);
|
myforf.datastack.push(a);
|
||||||
myforf.datastack.push(b);
|
myforf.datastack.push(b);
|
||||||
};
|
};
|
||||||
this.builtins["if"] = function(myforf) {
|
this.builtins["if"] = function(myforf) {
|
||||||
var ifclause = myforf.datastack.pop();
|
var ifclause = myforf.popData();
|
||||||
var cond = myforf.datastack.pop();
|
var cond = myforf.popData();
|
||||||
if (cond) {
|
if (cond) {
|
||||||
// TODO: make sure ifclause is a list
|
// TODO: make sure ifclause is a list
|
||||||
for (var i = 0; i < ifclause.length; i++) {
|
for (var i = 0; i < ifclause.length; i++) {
|
||||||
|
@ -143,9 +141,9 @@ var Forf = function() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.builtins["ifelse"] = function(myforf) {
|
this.builtins["ifelse"] = function(myforf) {
|
||||||
var elseclause = myforf.datastack.pop();
|
var elseclause = myforf.popData();
|
||||||
var ifclause = myforf.datastack.pop();
|
var ifclause = myforf.popData();
|
||||||
var cond = myforf.datastack.pop();
|
var cond = myforf.popData();
|
||||||
if (!cond) {
|
if (!cond) {
|
||||||
ifclause = elseclause;
|
ifclause = elseclause;
|
||||||
}
|
}
|
||||||
|
@ -155,15 +153,15 @@ var Forf = function() {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
this.builtins["mset"] = function(myforf) {
|
this.builtins["mset"] = function(myforf) {
|
||||||
var pos = myforf.datastack.pop();
|
var pos = myforf.popData();
|
||||||
var a = myforf.datastack.pop();
|
var a = myforf.popData();
|
||||||
if (pos < 0 || pos >= MEMORY_SIZE) {
|
if (pos < 0 || pos >= MEMORY_SIZE) {
|
||||||
throw "invalid memory location";
|
throw "invalid memory location";
|
||||||
}
|
}
|
||||||
myforf.mem[pos] = a;
|
myforf.mem[pos] = a;
|
||||||
};
|
};
|
||||||
this.builtins["mget"] = function(myforf) {
|
this.builtins["mget"] = function(myforf) {
|
||||||
var pos = myforf.datastack.pop();
|
var pos = myforf.popData();
|
||||||
if (pos < 0 || pos >= MEMORY_SIZE) {
|
if (pos < 0 || pos >= MEMORY_SIZE) {
|
||||||
throw "invalid memory location";
|
throw "invalid memory location";
|
||||||
}
|
}
|
||||||
|
@ -171,11 +169,20 @@ var Forf = function() {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Forf.prototype.popData = function() {
|
||||||
|
if (this.datastack.length === 0) {
|
||||||
|
throw "tried to pop from empty stack";
|
||||||
|
}
|
||||||
|
return this.datastack.pop();
|
||||||
|
};
|
||||||
|
|
||||||
Forf.prototype.init = function(code) {
|
Forf.prototype.init = function(code) {
|
||||||
this.code = code;
|
this.code = code;
|
||||||
};
|
};
|
||||||
|
|
||||||
Forf.prototype.parse = function() {
|
Forf.prototype.parse = function() {
|
||||||
|
this.cmdstack = [];
|
||||||
|
|
||||||
// 'parse' the input
|
// 'parse' the input
|
||||||
this.code = this.code.replace(/\([^)]*\)/g, "");
|
this.code = this.code.replace(/\([^)]*\)/g, "");
|
||||||
var splitCode = this.code.split(/([{}])/).join(" ");
|
var splitCode = this.code.split(/([{}])/).join(" ");
|
||||||
|
@ -214,6 +221,8 @@ Forf.prototype.parse = function() {
|
||||||
};
|
};
|
||||||
|
|
||||||
Forf.prototype.run = function() {
|
Forf.prototype.run = function() {
|
||||||
|
this.datastack = [];
|
||||||
|
|
||||||
var running = true;
|
var running = true;
|
||||||
while (running && this.cmdstack.length) {
|
while (running && this.cmdstack.length) {
|
||||||
var val = this.cmdstack.pop();
|
var val = this.cmdstack.pop();
|
||||||
|
@ -222,7 +231,7 @@ Forf.prototype.run = function() {
|
||||||
if (val in this.builtins) {
|
if (val in this.builtins) {
|
||||||
func(this);
|
func(this);
|
||||||
} else {
|
} else {
|
||||||
throw "no such function " + val;
|
throw "no such function '" + val + "'";
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.datastack.push(val);
|
this.datastack.push(val);
|
||||||
|
@ -258,12 +267,12 @@ var ForfTank = function() {
|
||||||
myforf.fire();
|
myforf.fire();
|
||||||
};
|
};
|
||||||
this.builtins["set-speed!"] = function(myforf) {
|
this.builtins["set-speed!"] = function(myforf) {
|
||||||
var right = myforf.datastack.pop();
|
var right = myforf.popData();
|
||||||
var left = myforf.datastack.pop();
|
var left = myforf.popData();
|
||||||
myforf.setSpeed(left, right);
|
myforf.setSpeed(left, right);
|
||||||
};
|
};
|
||||||
this.builtins["set-turret!"] = function(myforf) {
|
this.builtins["set-turret!"] = function(myforf) {
|
||||||
var angle = myforf.datastack.pop();
|
var angle = myforf.popData();
|
||||||
myforf.setTurret(deg2rad(angle));
|
myforf.setTurret(deg2rad(angle));
|
||||||
};
|
};
|
||||||
this.builtins["get-turret"] = function(myforf) {
|
this.builtins["get-turret"] = function(myforf) {
|
||||||
|
@ -271,15 +280,15 @@ var ForfTank = function() {
|
||||||
myforf.datastack.push(rad2deg(angle));
|
myforf.datastack.push(rad2deg(angle));
|
||||||
};
|
};
|
||||||
this.builtins["sensor?"] = function(myforf) {
|
this.builtins["sensor?"] = function(myforf) {
|
||||||
var sensor_num = myforf.datastack.pop();
|
var sensor_num = myforf.popData();
|
||||||
myforf.datastack.push(myforf.getSensor(sensor_num));
|
myforf.datastack.push(myforf.getSensor(sensor_num));
|
||||||
};
|
};
|
||||||
this.builtins["set-led!"] = function(myforf) {
|
this.builtins["set-led!"] = function(myforf) {
|
||||||
var active = myforf.datastack.pop();
|
var active = myforf.popData();
|
||||||
myforf.setLed(active);
|
myforf.setLed(active);
|
||||||
};
|
};
|
||||||
this.builtins["random"] = function(myforf) {
|
this.builtins["random"] = function(myforf) {
|
||||||
var max = myforf.datastack.pop();
|
var max = myforf.popData();
|
||||||
if (max < 1) {
|
if (max < 1) {
|
||||||
myforf.datastack.push(0);
|
myforf.datastack.push(0);
|
||||||
return;
|
return;
|
||||||
|
@ -658,12 +667,25 @@ var updateTanks = function(tanks) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Run programs */
|
/* Run programs */
|
||||||
|
var errors = [];
|
||||||
for (var i = 0; i < tanks.length; i++) {
|
for (var i = 0; i < tanks.length; i++) {
|
||||||
if (tanks[i].killer) {
|
if (tanks[i].killer) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
tanks[i].parse(tanks[i].code);
|
try {
|
||||||
tanks[i].run();
|
tanks[i].parse(tanks[i].code);
|
||||||
|
tanks[i].run();
|
||||||
|
} catch (e) {
|
||||||
|
errors.push(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (errors.length) {
|
||||||
|
if (interval) {
|
||||||
|
clearInterval(interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('debug').innerHTML = "Error: " + errors.join();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fire cannons and check for crashes */
|
/* Fire cannons and check for crashes */
|
||||||
|
@ -696,6 +718,8 @@ var resetTanks = function() {
|
||||||
clearInterval(interval);
|
clearInterval(interval);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.getElementById('debug').innerHTML = " ";
|
||||||
|
|
||||||
tanks = [];
|
tanks = [];
|
||||||
ftanks = [];
|
ftanks = [];
|
||||||
var tank;
|
var tank;
|
||||||
|
|
|
@ -36,6 +36,9 @@ cat <<EOF >$fn
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<title>Tanks Round $next</title>
|
<title>Tanks Round $next</title>
|
||||||
|
<link rel="stylesheet" href="//code.jquery.com/ui/1.11.0/themes/ui-darkness/jquery-ui.css" type="text/css">
|
||||||
|
<script src="//code.jquery.com/jquery-1.11.0.min.js"></script>
|
||||||
|
<script src="//code.jquery.com/ui/1.11.0/jquery-ui.min.js"></script>
|
||||||
<script type="application/javascript" src="tanks.js"></script>
|
<script type="application/javascript" src="tanks.js"></script>
|
||||||
<link rel="stylesheet" href="style.css" type="text/css">
|
<link rel="stylesheet" href="style.css" type="text/css">
|
||||||
<script type="application/javascript">
|
<script type="application/javascript">
|
||||||
|
|
|
@ -191,3 +191,10 @@ table.pollster thead {
|
||||||
.swatch {
|
.swatch {
|
||||||
color: #000000;
|
color: #000000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.ui-slider-tick-mark {
|
||||||
|
display: inline-block;
|
||||||
|
width: 2px;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
|
|
@ -25,7 +25,7 @@ BEGIN {
|
||||||
print " <h2>Rankings</h2>";
|
print " <h2>Rankings</h2>";
|
||||||
print " <p>Over the last " ngames" games only.</p>";
|
print " <p>Over the last " ngames" games only.</p>";
|
||||||
print " <ol>";
|
print " <ol>";
|
||||||
for (i = rounds - ngames - 1; i < rounds; i += 1) {
|
for (i = rounds - ngames - 1; i > 0 && i < rounds; i += 1) {
|
||||||
fn = sprintf("round-%04d.html", i)
|
fn = sprintf("round-%04d.html", i)
|
||||||
while (getline < fn) {
|
while (getline < fn) {
|
||||||
if ($2 == "score") {
|
if ($2 == "score") {
|
||||||
|
|
98
tanks.js
98
tanks.js
|
@ -62,11 +62,7 @@ function Tank(ctx, width, height, color, sensors) {
|
||||||
if (!this.dead) {
|
if (!this.dead) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.fire == 5) {
|
|
||||||
// one frame of cannon fire
|
|
||||||
this.draw_cannon();
|
|
||||||
this.fire = 0;
|
|
||||||
}
|
|
||||||
var points = 7;
|
var points = 7;
|
||||||
var angle = Math.PI / points;
|
var angle = Math.PI / points;
|
||||||
|
|
||||||
|
@ -74,6 +70,15 @@ function Tank(ctx, width, height, color, sensors) {
|
||||||
ctx.translate(this.x, this.y);
|
ctx.translate(this.x, this.y);
|
||||||
ctx.rotate(this.rotation);
|
ctx.rotate(this.rotation);
|
||||||
|
|
||||||
|
if (this.fire == 5) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.rotate(this.turret);
|
||||||
|
// one frame of cannon fire
|
||||||
|
this.draw_cannon();
|
||||||
|
this.fire = 0;
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
ctx.lineWidth = 2;
|
ctx.lineWidth = 2;
|
||||||
ctx.strokeStyle = craterStroke;
|
ctx.strokeStyle = craterStroke;
|
||||||
ctx.fillStyle = craterFill;
|
ctx.fillStyle = craterFill;
|
||||||
|
@ -173,10 +178,21 @@ function Tank(ctx, width, height, color, sensors) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var loop_id;
|
||||||
|
var updateFunc = null;
|
||||||
|
function togglePlayback() {
|
||||||
|
if ($("#playing").prop("checked")) {
|
||||||
|
loop_id = setInterval(updateFunc, 66);
|
||||||
|
} else {
|
||||||
|
clearInterval(loop_id);
|
||||||
|
loop_id = null;
|
||||||
|
}
|
||||||
|
$("#pauselabel").toggleClass("ui-icon-play ui-icon-pause");
|
||||||
|
}
|
||||||
|
|
||||||
function start(id, game) {
|
function start(id, game) {
|
||||||
var canvas = document.getElementById(id);
|
var canvas = document.getElementById(id);
|
||||||
var ctx = canvas.getContext('2d');
|
var ctx = canvas.getContext('2d');
|
||||||
var loop_id;
|
|
||||||
|
|
||||||
canvas.width = game[0][0];
|
canvas.width = game[0][0];
|
||||||
canvas.height = game[0][1];
|
canvas.height = game[0][1];
|
||||||
|
@ -199,15 +215,7 @@ function start(id, game) {
|
||||||
lastframe = frame;
|
lastframe = frame;
|
||||||
}
|
}
|
||||||
|
|
||||||
function update() {
|
function drawFrame(idx) {
|
||||||
var idx = frame % (turns.length + 20);
|
|
||||||
var turn;
|
|
||||||
|
|
||||||
frame += 1;
|
|
||||||
if (idx >= turns.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
canvas.width = canvas.width;
|
canvas.width = canvas.width;
|
||||||
turn = turns[idx];
|
turn = turns[idx];
|
||||||
|
|
||||||
|
@ -231,12 +239,72 @@ function start(id, game) {
|
||||||
for (i in turn) {
|
for (i in turn) {
|
||||||
tanks[i].draw_tank()
|
tanks[i].draw_tank()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
document.getElementById('frameid').innerHTML = idx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function update() {
|
||||||
|
var idx = frame % (turns.length + 20);
|
||||||
|
var turn;
|
||||||
|
|
||||||
|
frame += 1;
|
||||||
|
if (idx >= turns.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawFrame(idx);
|
||||||
|
|
||||||
|
$('#seekslider').slider('value', idx);
|
||||||
|
}
|
||||||
|
|
||||||
|
function seekToFrame(newidx) {
|
||||||
|
var idx = frame % (turns.length + 20);
|
||||||
|
if (idx !== newidx) {
|
||||||
|
frame = newidx;
|
||||||
|
drawFrame(newidx);
|
||||||
|
}
|
||||||
|
// make sure we're paused
|
||||||
|
if ($("#playing").prop("checked")) {
|
||||||
|
$("#playing").prop("checked", false);
|
||||||
|
togglePlayback();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFunc = update;
|
||||||
loop_id = setInterval(update, 66);
|
loop_id = setInterval(update, 66);
|
||||||
//loop_id = setInterval(update, 400);
|
//loop_id = setInterval(update, 400);
|
||||||
if (fps) {
|
if (fps) {
|
||||||
setInterval(update_fps, 1000);
|
setInterval(update_fps, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (id === "battlefield") {
|
||||||
|
$("#game_box").append('<p><input type="checkbox" checked id="playing" onclick="togglePlayback();"><label for="playing"><span class="ui-icon ui-icon-pause" id="pauselabel"></class></label> <span id="frameid">0</span> <span id="seekslider" style="width: 75%; float: right;"></span></p>');
|
||||||
|
$('#playing').button();
|
||||||
|
var slider = $('#seekslider');
|
||||||
|
slider.slider({ max: turns.length-1, slide: function(event, ui) { seekToFrame(ui.value); } });
|
||||||
|
|
||||||
|
var spacing = 100 / turns.length;
|
||||||
|
var deaths = [];
|
||||||
|
for (i in turns[0]) {
|
||||||
|
deaths.push(false);
|
||||||
|
}
|
||||||
|
var percent = 0;
|
||||||
|
for (var f = 0; f < turns.length; f++) {
|
||||||
|
var turn = turns[f];
|
||||||
|
if (percent < (spacing * f)) {
|
||||||
|
percent = spacing * f;
|
||||||
|
}
|
||||||
|
for (var i = 0; i < turn.length; i++) {
|
||||||
|
if (deaths[i]) { continue; }
|
||||||
|
if (!turn[i] || (turn[i][4] & 4)) {
|
||||||
|
deaths[i] = true;
|
||||||
|
// http://stackoverflow.com/questions/8648963/add-tick-marks-to-jquery-slider
|
||||||
|
$('<span class="ui-slider-tick-mark"></span>').css('left', percent + '%').css('background-color', game[1][i][0]).appendTo(slider);
|
||||||
|
percent++;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue