mirror of
https://github.com/logsol/chuck.js.git
synced 2026-05-11 10:37:34 +00:00
added max user and refactored coordinator/serveruser a bit fixes #105
This commit is contained in:
parent
ccd146f01b
commit
6b472dc134
9 changed files with 156 additions and 66 deletions
|
|
@ -53,6 +53,7 @@ function (ProtocolHelper, GameController, User, Nc, Settings, DomController) {
|
||||||
this.sendCommand('join', options);
|
this.sendCommand('join', options);
|
||||||
DomController.setConnected(true);
|
DomController.setConnected(true);
|
||||||
} else {
|
} else {
|
||||||
|
alert("Error: no channel name");
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -80,7 +81,7 @@ function (ProtocolHelper, GameController, User, Nc, Settings, DomController) {
|
||||||
}
|
}
|
||||||
|
|
||||||
Networker.prototype.onJoinError = function(options) {
|
Networker.prototype.onJoinError = function(options) {
|
||||||
// alert(options.message);
|
alert(options.message);
|
||||||
window.location.href = "/";
|
window.location.href = "/";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -76,6 +76,7 @@ define(function() {
|
||||||
NETWORK_LOG_FILTER: ['ping', 'pong', 'worldUpdate', 'lookAt'],
|
NETWORK_LOG_FILTER: ['ping', 'pong', 'worldUpdate', 'lookAt'],
|
||||||
|
|
||||||
// CHANNEL
|
// CHANNEL
|
||||||
|
CHANNEL_MAX_USERS: 20,
|
||||||
CHANNEL_DESTRUCTION_TIME: 5 * 60,
|
CHANNEL_DESTRUCTION_TIME: 5 * 60,
|
||||||
CHANNEL_END_ROUND_TIME: 4, //10,
|
CHANNEL_END_ROUND_TIME: 4, //10,
|
||||||
CHANNEL_DEFAULT_MAX_USERS: 40,
|
CHANNEL_DEFAULT_MAX_USERS: 40,
|
||||||
|
|
|
||||||
|
|
@ -144,11 +144,19 @@ function (ColorConverter, Exception, PointerLockManager, Qs) {
|
||||||
var html = "";
|
var html = "";
|
||||||
if(list.length > 0) {
|
if(list.length > 0) {
|
||||||
for (var i = 0; i < list.length; i++) {
|
for (var i = 0; i < list.length; i++) {
|
||||||
|
|
||||||
var channel = list[i];
|
var channel = list[i];
|
||||||
html += "<tr>";
|
var fullState = channel.playerCount >= channel.maxUsers;
|
||||||
html += "<td><a href='#" + channel.name + "'>" + channel.name + "</a></td>";
|
var fullString = fullState ? " Full" : "";
|
||||||
|
var fullStyle = fullState ? 'class="full"' : "";
|
||||||
|
var players = channel.playerCount
|
||||||
|
? "<span id='players'>Player:<br>- " + channel.players.join("<br>- ") + "</span>"
|
||||||
|
: "";
|
||||||
|
|
||||||
|
html += "<tr "+fullStyle+">";
|
||||||
|
html += "<td><a href='#" + channel.channelName + "'>" + channel.channelName + "</a></td>";
|
||||||
html += "<td>death match</td>";
|
html += "<td>death match</td>";
|
||||||
html += "<td>" + channel.playerCount + "</td>";
|
html += "<td class='playersCell'>" + channel.playerCount + fullString + players + "</td>";
|
||||||
html += "</tr>";
|
html += "</tr>";
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -190,8 +198,15 @@ function (ColorConverter, Exception, PointerLockManager, Qs) {
|
||||||
|
|
||||||
Qs.$("form#createform").onsubmit = function(e) {
|
Qs.$("form#createform").onsubmit = function(e) {
|
||||||
try {
|
try {
|
||||||
var channelName = Qs.$("#customname").value;
|
|
||||||
create(channelName, onCreateSuccess);
|
var options = {
|
||||||
|
channelName: Qs.$("#customname").value,
|
||||||
|
levelUids: getSelectedMaps(),
|
||||||
|
maxUsers: parseInt(Qs.$("#userLimit").value, 10),
|
||||||
|
scoreLimit: parseInt(Qs.$("#scoreLimit").value, 10)
|
||||||
|
};
|
||||||
|
|
||||||
|
create(options, onCreateSuccess);
|
||||||
} catch(e) {
|
} catch(e) {
|
||||||
console.error(e)
|
console.error(e)
|
||||||
}
|
}
|
||||||
|
|
@ -246,7 +261,15 @@ function (ColorConverter, Exception, PointerLockManager, Qs) {
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!channelExists(list, defaultChannelName)) {
|
if(!channelExists(list, defaultChannelName)) {
|
||||||
create(defaultChannelName, function() {
|
|
||||||
|
var options = {
|
||||||
|
channelName: defaultChannelName,
|
||||||
|
levelUids: getSelectedMaps(),
|
||||||
|
maxUsers: parseInt(Qs.$("#userLimit").value, 10),
|
||||||
|
scoreLimit: parseInt(Qs.$("#scoreLimit").value, 10)
|
||||||
|
};
|
||||||
|
|
||||||
|
create(options, function() {
|
||||||
join(nickname, defaultChannelName); // only called on success
|
join(nickname, defaultChannelName); // only called on success
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -283,7 +306,7 @@ function (ColorConverter, Exception, PointerLockManager, Qs) {
|
||||||
function channelExists(list, channelName) {
|
function channelExists(list, channelName) {
|
||||||
for (var i = 0; i < list.length; i++) {
|
for (var i = 0; i < list.length; i++) {
|
||||||
var channel = list[i];
|
var channel = list[i];
|
||||||
if(channel.name == channelName) {
|
if(channel.channelName == channelName) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -302,18 +325,33 @@ function (ColorConverter, Exception, PointerLockManager, Qs) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateForCreate(channelName, maps) {
|
function validateForCreate(options) {
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
if(maps.length < 1) {
|
// great validation on server side does it all.
|
||||||
|
|
||||||
|
/*
|
||||||
|
if(options.levelUids.length < 1) {
|
||||||
alert("Please choose at least one map.")
|
alert("Please choose at least one map.")
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!channelName) {
|
if(!options.channelName || options.channelName.length < 3) {
|
||||||
alert("Please provide a channel name.")
|
alert("Please provide a channel name of at least 3 characters.")
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!parseInt(options.maxUsers) > 1 || !parseInt(options.maxUsers) < 20) {
|
||||||
|
alert("Number of users must be larger than 1 and smaller than 20.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!parseInt(options.scoreLimit) > 1 || !parseInt(options.scoreLimit) < 99) {
|
||||||
|
alert("Score limit must be larger than 1 and smaller than 99.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSelectedMaps() {
|
function getSelectedMaps() {
|
||||||
|
|
@ -367,20 +405,12 @@ function (ColorConverter, Exception, PointerLockManager, Qs) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function create(channelName, callback) {
|
function create(options, callback) {
|
||||||
var maps = getSelectedMaps();
|
|
||||||
|
|
||||||
if(validateForCreate(channelName, maps)) {
|
if(validateForCreate(options)) {
|
||||||
|
|
||||||
var options = {
|
options["minUsers"] = 1;
|
||||||
channelName: channelName,
|
localStorage["customname"] = options.channelName;
|
||||||
levelUids: maps,
|
|
||||||
maxUsers: 10,
|
|
||||||
minUsers: 2,
|
|
||||||
scoreLimit: parseInt(Qs.$("#scoreLimit").value, 10)
|
|
||||||
}
|
|
||||||
|
|
||||||
localStorage["customname"] = channelName;
|
|
||||||
|
|
||||||
ajax("createChannel", options, function(responseText) {
|
ajax("createChannel", options, function(responseText) {
|
||||||
if(typeof callback == 'function') {
|
if(typeof callback == 'function') {
|
||||||
|
|
|
||||||
|
|
@ -96,22 +96,23 @@ function (Nc, ProtocolHelper, validate, Options, Settings, FileSystem) {
|
||||||
newOptions.maxUsers = options.maxUsers;
|
newOptions.maxUsers = options.maxUsers;
|
||||||
} else {
|
} else {
|
||||||
this.isError = true;
|
this.isError = true;
|
||||||
return "Could not create channel, Max users invalid. (Limited to " + Settings.CHANNEL_MAX_USERS + " users)";
|
return "Could not create channel, user limit invalid. Limited to " + Settings.CHANNEL_MAX_USERS + " users";
|
||||||
}
|
}
|
||||||
|
|
||||||
if(validate(options.minUsers, {optional: true, type: 'number', min: 0, max: Settings.CHANNEL_MAX_USERS})) {
|
if(validate(options.minUsers, {optional: true, type: 'number', min: 0, max: Settings.CHANNEL_MAX_USERS})) {
|
||||||
newOptions.minUsers = options.minUsers;
|
newOptions.minUsers = options.minUsers;
|
||||||
} else {
|
} else {
|
||||||
this.isError = true;
|
this.isError = true;
|
||||||
return "Could not create channel, Max users too high. Limited to: " + Settings.CHANNEL_MAX_USERS;
|
return "Could not create channel, minimal users limit too high. Limited to: " + Settings.CHANNEL_MAX_USERS;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Limits
|
// Limits
|
||||||
if(validate(options.scoreLimit, {type: 'number', min: 1, max: 999})) {
|
var scoreLimitPreferences = {type: 'number', min: 1, max: 999};
|
||||||
|
if(validate(options.scoreLimit, scoreLimitPreferences)) {
|
||||||
newOptions.scoreLimit = options.scoreLimit;
|
newOptions.scoreLimit = options.scoreLimit;
|
||||||
} else {
|
} else {
|
||||||
this.isError = true;
|
this.isError = true;
|
||||||
return "Could not create channel, score limit (" + options.scoreLimit + ").";
|
return "Could not create channel, score limit (" + options.scoreLimit + ") must be between " + scoreLimitPreferences.min + " and " + scoreLimitPreferences.max;
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultOptions = {
|
var defaultOptions = {
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ function (User, PipeToChannel, Nc, Settings) {
|
||||||
|
|
||||||
function Coordinator() {
|
function Coordinator() {
|
||||||
this.channelPipes = {};
|
this.channelPipes = {};
|
||||||
this.users = [];
|
|
||||||
|
|
||||||
Nc.on(Nc.ns.server.events.controlCommand.coordinator, this.onMessage, this);
|
Nc.on(Nc.ns.server.events.controlCommand.coordinator, this.onMessage, this);
|
||||||
|
|
||||||
|
|
@ -19,21 +18,12 @@ function (User, PipeToChannel, Nc, Settings) {
|
||||||
}
|
}
|
||||||
|
|
||||||
Coordinator.prototype.createUser = function (socketLink) {
|
Coordinator.prototype.createUser = function (socketLink) {
|
||||||
this.users.push(new User(socketLink, this));
|
new User(socketLink, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
Coordinator.prototype.removeUser = function (user) {
|
// was assignUserToChannel...
|
||||||
for(var i = 0; i < this.users.length; i++) {
|
Coordinator.prototype.getChannelPipeByName = function (channelName) {
|
||||||
if(this.users[i] === user) {
|
return this.channelPipes[channelName];
|
||||||
this.users.splice(i, 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Coordinator.prototype.assignUserToChannel = function (user, channelName) {
|
|
||||||
var channelPipe = this.channelPipes[channelName];
|
|
||||||
user.setChannelPipe(channelPipe);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Coordinator.prototype.onDestroyPipe = function(channelName) {
|
Coordinator.prototype.onDestroyPipe = function(channelName) {
|
||||||
|
|
@ -44,18 +34,17 @@ function (User, PipeToChannel, Nc, Settings) {
|
||||||
var list = [];
|
var list = [];
|
||||||
for (var channelName in this.channelPipes) {
|
for (var channelName in this.channelPipes) {
|
||||||
|
|
||||||
var count = 0;
|
var options = this.channelPipes[channelName].options;
|
||||||
|
|
||||||
for(var i = 0; i < this.users.length; i++) {
|
var playerNames = [];
|
||||||
if(this.users[i].channelPipe === this.channelPipes[channelName]){
|
var users = this.channelPipes[channelName].getUsers();
|
||||||
count++;
|
for (var i = 0; i < users.length; i++) {
|
||||||
}
|
playerNames[i] = users[i].options.nickname;
|
||||||
}
|
};
|
||||||
|
options.players = playerNames;
|
||||||
|
options.playerCount = options.players.length;
|
||||||
|
|
||||||
list.push({
|
list.push(options);
|
||||||
name: channelName,
|
|
||||||
playerCount: count
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,8 @@ function (Nc, childProcess) {
|
||||||
function PipeToChannel (options) {
|
function PipeToChannel (options) {
|
||||||
|
|
||||||
this.fork = null;
|
this.fork = null;
|
||||||
|
this.options = options;
|
||||||
|
this.users = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.fork = fork('channel.js'
|
this.fork = fork('channel.js'
|
||||||
|
|
@ -28,8 +30,6 @@ function (Nc, childProcess) {
|
||||||
this.send('channel/' + options.channelName, { CREATE: true, options: options });
|
this.send('channel/' + options.channelName, { CREATE: true, options: options });
|
||||||
|
|
||||||
this.fork.on('message', this.onMessage.bind(this));
|
this.fork.on('message', this.onMessage.bind(this));
|
||||||
|
|
||||||
var self = this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// While creating user
|
// While creating user
|
||||||
|
|
@ -52,6 +52,10 @@ function (Nc, childProcess) {
|
||||||
this.fork.send(message);
|
this.fork.send(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PipeToChannel.prototype.isFull = function() {
|
||||||
|
return this.users.length >= this.options.maxUsers;
|
||||||
|
};
|
||||||
|
|
||||||
PipeToChannel.prototype.onMessage = function (message) {
|
PipeToChannel.prototype.onMessage = function (message) {
|
||||||
switch(message.recipient) {
|
switch(message.recipient) {
|
||||||
case 'coordinator':
|
case 'coordinator':
|
||||||
|
|
@ -64,6 +68,26 @@ function (Nc, childProcess) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PipeToChannel.prototype.addUser = function(user) {
|
||||||
|
this.users.push(user);
|
||||||
|
this.send('channel', { addUser: user.options });
|
||||||
|
};
|
||||||
|
|
||||||
|
PipeToChannel.prototype.removeUser = function(user) {
|
||||||
|
for(var i = 0; i < this.users.length; i++) {
|
||||||
|
if(this.users[i] === user) {
|
||||||
|
this.users.splice(i, 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.send('channel', { releaseUser: user.id });
|
||||||
|
};
|
||||||
|
|
||||||
|
PipeToChannel.prototype.getUsers = function() {
|
||||||
|
return this.users;
|
||||||
|
};
|
||||||
|
|
||||||
return PipeToChannel;
|
return PipeToChannel;
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
@ -14,6 +14,7 @@ function (Parent, ProtocolHelper, Nc) {
|
||||||
this.coordinator = coordinator;
|
this.coordinator = coordinator;
|
||||||
this.socketLink = socketLink;
|
this.socketLink = socketLink;
|
||||||
this.channelPipe = null;
|
this.channelPipe = null;
|
||||||
|
this.options = null;
|
||||||
|
|
||||||
socketLink.on('message', this.onMessage.bind(this));
|
socketLink.on('message', this.onMessage.bind(this));
|
||||||
socketLink.on('disconnect', this.onDisconnect.bind(this));
|
socketLink.on('disconnect', this.onDisconnect.bind(this));
|
||||||
|
|
@ -23,15 +24,23 @@ function (Parent, ProtocolHelper, Nc) {
|
||||||
|
|
||||||
User.prototype = Object.create(Parent.prototype);
|
User.prototype = Object.create(Parent.prototype);
|
||||||
|
|
||||||
|
/*
|
||||||
User.prototype.setChannelPipe = function(channelPipe) {
|
User.prototype.setChannelPipe = function(channelPipe) {
|
||||||
if(channelPipe) {
|
if(channelPipe) {
|
||||||
this.channelPipe = channelPipe;
|
if (channelPipe.isWithinUserLimit()) {
|
||||||
|
this.channelPipe = channelPipe;
|
||||||
|
this.channelPipe.addUser(this);
|
||||||
|
} else {
|
||||||
|
var message = ProtocolHelper.encodeCommand("joinError", {message:"Channel is full"});
|
||||||
|
this.socketLink.send(message);
|
||||||
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
var message = ProtocolHelper.encodeCommand("joinError", {message:"Channel not found"});
|
var message = ProtocolHelper.encodeCommand("joinError", {message:"Channel not found"});
|
||||||
this.socketLink.send(message);
|
this.socketLink.send(message);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
*/
|
||||||
|
|
||||||
// Socket callbacks
|
// Socket callbacks
|
||||||
|
|
||||||
|
|
@ -41,14 +50,12 @@ function (Parent, ProtocolHelper, Nc) {
|
||||||
|
|
||||||
User.prototype.onDisconnect = function () {
|
User.prototype.onDisconnect = function () {
|
||||||
|
|
||||||
this.coordinator.removeUser(this);
|
|
||||||
|
|
||||||
if(!this.channelPipe) {
|
if(!this.channelPipe) {
|
||||||
console.warn("Disconnecting user without a channel.");
|
console.warn("Disconnecting user without a channel. (Maybe channel was full)");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.channelPipe.send('channel', { releaseUser: this.id });
|
this.channelPipe.removeUser(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -56,18 +63,29 @@ function (Parent, ProtocolHelper, Nc) {
|
||||||
// Remember: control commands are coordinator relevant commands
|
// Remember: control commands are coordinator relevant commands
|
||||||
|
|
||||||
User.prototype.onJoin = function(options) {
|
User.prototype.onJoin = function(options) {
|
||||||
this.coordinator.assignUserToChannel(this, options.channelName);
|
|
||||||
|
|
||||||
if(!this.channelPipe) {
|
var channelPipe = this.coordinator.getChannelPipeByName(options.channelName);
|
||||||
console.warn("Can not join user because channel (" + options.channelName + ") does not exist.")
|
|
||||||
|
if(!channelPipe) {
|
||||||
|
var message = ProtocolHelper.encodeCommand("joinError", {message:"Channel " + options.channelName + " not found."});
|
||||||
|
this.socketLink.send(message);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (channelPipe.isFull()) {
|
||||||
|
var message = ProtocolHelper.encodeCommand("joinError", {message:"Sorry! Channel " + options.channelName + " is full."});
|
||||||
|
this.socketLink.send(message);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.channelPipe = channelPipe;
|
||||||
|
|
||||||
var userOptions = {
|
var userOptions = {
|
||||||
id: this.id,
|
id: this.id,
|
||||||
nickname: options.nickname
|
nickname: options.nickname
|
||||||
}
|
}
|
||||||
this.channelPipe.send('channel', { addUser: userOptions });
|
this.options = userOptions;
|
||||||
|
this.channelPipe.addUser(this);
|
||||||
};
|
};
|
||||||
|
|
||||||
/* FIXME: watch out and check in wich direction game and control commands flow */
|
/* FIXME: watch out and check in wich direction game and control commands flow */
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ body {
|
||||||
background: #222;
|
background: #222;
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
font-family: 'Joystix', "Lucida Grande", sans-serif;
|
font-family: 'Joystix', "Lucida Grande", sans-serif;
|
||||||
text-transform: uppercase;
|
/*text-transform: uppercase;*/
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
display: table;
|
display: table;
|
||||||
|
|
@ -113,7 +113,7 @@ article#menu {
|
||||||
margin: 4em auto;
|
margin: 4em auto;
|
||||||
background: #1a1a1a;
|
background: #1a1a1a;
|
||||||
padding: 2em;
|
padding: 2em;
|
||||||
max-width: 30em;
|
max-width: 40em;
|
||||||
}
|
}
|
||||||
|
|
||||||
table, th, td {
|
table, th, td {
|
||||||
|
|
@ -142,6 +142,31 @@ tr:hover td {
|
||||||
background: #222;
|
background: #222;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.full {
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
|
.full a {
|
||||||
|
color: inherit;
|
||||||
|
cursor:not-allowed;;
|
||||||
|
}
|
||||||
|
|
||||||
|
#players {
|
||||||
|
position: absolute;
|
||||||
|
|
||||||
|
border: 1px solid #777;
|
||||||
|
padding: 20px;
|
||||||
|
margin-left: 35px;
|
||||||
|
margin-top: -10px;
|
||||||
|
background: rgba(20, 20, 20, 0.95);
|
||||||
|
box-shadow: 5px 5px 5px #000;
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.playersCell:hover #players {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
a {
|
a {
|
||||||
color: #ccc;
|
color: #ccc;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@
|
||||||
<h2>Create your own!</h2>
|
<h2>Create your own!</h2>
|
||||||
<p><label>Name:<br> <input id="customname"></label></p>
|
<p><label>Name:<br> <input id="customname"></label></p>
|
||||||
<p><label>Score limit:<br> <input id="scoreLimit" type="number" value="5"></label></p>
|
<p><label>Score limit:<br> <input id="scoreLimit" type="number" value="5"></label></p>
|
||||||
|
<p><label>User limit:<br> <input id="userLimit" type="number" value="10"></label></p>
|
||||||
<fieldset>
|
<fieldset>
|
||||||
<legend>Maps</legend>
|
<legend>Maps</legend>
|
||||||
<ul id="maps">
|
<ul id="maps">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue