SSL now working and reorganised web server

This commit is contained in:
simon 2018-04-28 12:06:55 -04:00
parent e3a650a4fc
commit e025b6d2da
17 changed files with 380 additions and 265 deletions

View File

@ -48,7 +48,7 @@ public class GameServer implements Runnable {
try{ try{
mongo = new MongoClient("localhost", 27017); mongo = new MongoClient("localhost", 27017);
userManager = new UserManager(mongo); userManager = new UserManager(mongo, config);
} catch (UnknownHostException e) { } catch (UnknownHostException e) {
e.printStackTrace(); e.printStackTrace();
@ -199,7 +199,7 @@ public class GameServer implements Runnable {
LogManager.LOGGER.info("Loading all data from MongoDB"); LogManager.LOGGER.info("Loading all data from MongoDB");
DB db = mongo.getDB("mar"); DB db = mongo.getDB(config.getString("mongo_dbname"));
DBCollection worlds = db.getCollection("world"); DBCollection worlds = db.getCollection("world");
DBCollection server = db.getCollection("server"); DBCollection server = db.getCollection("server");
@ -235,7 +235,7 @@ public class GameServer implements Runnable {
LogManager.LOGGER.info("Saving to MongoDB | W:" + gameUniverse.getWorldCount() + " | U:" + gameUniverse.getUserCount()); LogManager.LOGGER.info("Saving to MongoDB | W:" + gameUniverse.getWorldCount() + " | U:" + gameUniverse.getUserCount());
try{ try{
DB db = mongo.getDB("mar"); DB db = mongo.getDB(config.getString("mongo_dbname"));
int unloaded_worlds = 0; int unloaded_worlds = 0;

View File

@ -1,19 +1,7 @@
package net.simon987.server; package net.simon987.server;
import net.simon987.server.logging.LogManager; import net.simon987.server.logging.LogManager;
import net.simon987.server.user.RegistrationException; import net.simon987.server.web.WebServer;
import net.simon987.server.web.AlertMessage;
import net.simon987.server.web.AlertType;
import net.simon987.server.websocket.SocketServer;
import org.apache.velocity.app.VelocityEngine;
import org.json.simple.JSONObject;
import spark.ModelAndView;
import spark.Spark;
import spark.template.velocity.VelocityTemplateEngine;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
public class Main { public class Main {
@ -24,184 +12,10 @@ public class Main {
//Load //Load
GameServer.INSTANCE.load(); GameServer.INSTANCE.load();
(new Thread(GameServer.INSTANCE)).start(); (new Thread(GameServer.INSTANCE)).start();
//TEST --------------------------- //Web server
Properties properties = new Properties(); new WebServer(GameServer.INSTANCE.getConfig());
properties.setProperty("file.resource.loader.path", "templates/");
VelocityTemplateEngine templateEngine = new VelocityTemplateEngine(new VelocityEngine(properties));
//--
//Websocket
Spark.webSocket("/socket", SocketServer.class);
Spark.staticFiles.externalLocation("static");
Spark.get("/", (request, response) -> {
Map<String, Object> model = new HashMap<>();
model.put("session", request.session());
return new ModelAndView(model, "home.vm");
}, templateEngine);
Spark.get("/leaderboard", (request, response) -> {
Map<String, Object> model = new HashMap<>();
model.put("session", request.session());
return new ModelAndView(new HashMap<>(), "leaderboard.vm");
}, templateEngine);
Spark.get("/play", (request, response) -> {
Map<String, Object> model = new HashMap<>();
model.put("session", request.session());
return new ModelAndView(model, "play.vm");
}, templateEngine);
Spark.get("/account", (request, response) -> {
Map<String, Object> model = new HashMap<>();
model.put("session", request.session());
if (request.session().attribute("username") != null) {
model.put("user", GameServer.INSTANCE.getGameUniverse().getUser(request.session().attribute("username")));
}
return new ModelAndView(model, "account.vm");
}, templateEngine);
Spark.post("/register", (request, response) -> {
String username = request.queryParams("username");
String password = request.queryParams("password");
if (username != null && password != null) {
try {
GameServer.INSTANCE.getUserManager().registerUser(username, password);
AlertMessage[] messages = {new AlertMessage("Successfully registered", AlertType.SUCCESS)};
request.session().attribute("messages", messages);
request.session().attribute("username", username);
LogManager.LOGGER.fine("(Web) " + username + " registered " + request.ip());
} catch (RegistrationException e) {
AlertMessage[] messages = {new AlertMessage(e.getMessage(), AlertType.DANGER)};
request.session().attribute("messages", messages);
}
}
response.redirect("/account");
return null;
});
Spark.post("/login", (request, response) -> {
String username = request.queryParams("username");
String password = request.queryParams("password");
if (username != null && password != null) {
if (GameServer.INSTANCE.getUserManager().validateUser(username, password)) {
AlertMessage[] messages = {new AlertMessage("Logged in as " + username, AlertType.SUCCESS)};
request.session().attribute("messages", messages);
request.session().attribute("username", username);
LogManager.LOGGER.fine("(Web) " + username + " logged in");
} else {
AlertMessage[] messages = {new AlertMessage("Invalid username or password", AlertType.DANGER)};
request.session().attribute("messages", messages);
}
}
response.redirect("/account");
return null;
});
Spark.get("logout", (request, response) -> {
AlertMessage[] messages = {new AlertMessage("Logged out", AlertType.INFO)};
request.session().attribute("messages", messages);
request.session().removeAttribute("username");
response.redirect("/account");
return null;
});
Spark.post("change_password", (request, response) -> {
String username = request.session().attribute("username");
String currentPassword = request.queryParams("password");
String newPassword = request.queryParams("new_password");
String newPasswordRepeat = request.queryParams("new_password_repeat");
if (newPassword.equals(newPasswordRepeat)) {
if (username != null && GameServer.INSTANCE.getUserManager().validateUser(username, currentPassword)) {
try {
GameServer.INSTANCE.getUserManager().changePassword(username, newPassword);
AlertMessage[] messages = {new AlertMessage("Changed password", AlertType.SUCCESS)};
request.session().attribute("messages", messages);
} catch (RegistrationException e) {
AlertMessage[] messages = {new AlertMessage(e.getMessage(), AlertType.DANGER)};
request.session().attribute("messages", messages);
}
} else {
AlertMessage[] messages = {new AlertMessage("Invalid password", AlertType.DANGER)};
request.session().attribute("messages", messages);
}
} else {
AlertMessage[] messages = {new AlertMessage("Passwords did not match", AlertType.DANGER)};
request.session().attribute("messages", messages);
}
response.redirect("/account");
return null;
});
Spark.get("/server_info", (request, response) -> {
//TODO put that in a constructor somewhere
String address;
if (GameServer.INSTANCE.getConfig().getInt("use_ssl") == 0) {
address = "ws://" +
GameServer.INSTANCE.getConfig().getString("mar_address") + ":" +
GameServer.INSTANCE.getConfig().getString("mar_port") + "/socket";
} else {
address = "wss://" +
GameServer.INSTANCE.getConfig().getString("mar_address") + ":" +
GameServer.INSTANCE.getConfig().getString("mar_port") + "/socket";
}
String serverName = GameServer.INSTANCE.getConfig().getString("server_name");
int tickLength = GameServer.INSTANCE.getConfig().getInt("tick_length");
JSONObject json = new JSONObject();
String username = request.session().attribute("username");
if (username != null) {
String token = GameServer.INSTANCE.getUserManager().generateAndGetToken(username);
json.put("token", token);
json.put("username", username);
} else {
json.put("token", "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000");
json.put("username", "guest");
}
json.put("address", address);
json.put("serverName", serverName);
json.put("tickLength", tickLength);
response.header("Content-Type", "application/json");
return json.toJSONString();
});
Spark.after((request, response) -> response.header("Content-Encoding", "gzip"));
} }
} }

View File

@ -56,7 +56,7 @@ public class GameUniverse {
*/ */
private World loadWorld(int x, int y, String dimension) { private World loadWorld(int x, int y, String dimension) {
DB db = mongo.getDB("mar"); DB db = mongo.getDB(GameServer.INSTANCE.getConfig().getString("mongo_dbname"));
DBCollection worlds = db.getCollection("world"); DBCollection worlds = db.getCollection("world");
BasicDBObject whereQuery = new BasicDBObject(); BasicDBObject whereQuery = new BasicDBObject();

View File

@ -2,6 +2,7 @@ package net.simon987.server.user;
import com.mongodb.*; import com.mongodb.*;
import net.simon987.server.GameServer; import net.simon987.server.GameServer;
import net.simon987.server.ServerConfiguration;
import net.simon987.server.assembly.exception.CancelledException; import net.simon987.server.assembly.exception.CancelledException;
import net.simon987.server.crypto.RandomStringGenerator; import net.simon987.server.crypto.RandomStringGenerator;
import net.simon987.server.logging.LogManager; import net.simon987.server.logging.LogManager;
@ -14,10 +15,10 @@ public class UserManager {
private MongoClient mongo; private MongoClient mongo;
private DBCollection userCollection; private DBCollection userCollection;
public UserManager(MongoClient mongo) { public UserManager(MongoClient mongo, ServerConfiguration config) {
this.mongo = mongo; this.mongo = mongo;
DB db = mongo.getDB("mar"); DB db = mongo.getDB(config.getString("mongo_dbname"));
userCollection = db.getCollection("user"); userCollection = db.getCollection("user");
} }

View File

@ -0,0 +1,25 @@
package net.simon987.server.web;
import net.simon987.server.GameServer;
import spark.ModelAndView;
import spark.Request;
import spark.Response;
import spark.TemplateViewRoute;
import java.util.HashMap;
import java.util.Map;
public class AccountPage implements TemplateViewRoute {
@Override
public ModelAndView handle(Request request, Response response) {
Map<String, Object> model = new HashMap<>();
model.put("session", request.session());
if (request.session().attribute("username") != null) {
model.put("user", GameServer.INSTANCE.getGameUniverse().getUser(request.session().attribute("username")));
}
return new ModelAndView(model, "account.vm");
}
}

View File

@ -0,0 +1,44 @@
package net.simon987.server.web;
import net.simon987.server.GameServer;
import net.simon987.server.user.RegistrationException;
import spark.ModelAndView;
import spark.Request;
import spark.Response;
import spark.Route;
public class ChangePasswordRoute implements Route {
@Override
public ModelAndView handle(Request request, Response response) {
String username = request.session().attribute("username");
String currentPassword = request.queryParams("password");
String newPassword = request.queryParams("new_password");
String newPasswordRepeat = request.queryParams("new_password_repeat");
if (newPassword.equals(newPasswordRepeat)) {
if (username != null && GameServer.INSTANCE.getUserManager().validateUser(username, currentPassword)) {
try {
GameServer.INSTANCE.getUserManager().changePassword(username, newPassword);
AlertMessage[] messages = {new AlertMessage("Changed password", AlertType.SUCCESS)};
request.session().attribute("messages", messages);
} catch (RegistrationException e) {
AlertMessage[] messages = {new AlertMessage(e.getMessage(), AlertType.DANGER)};
request.session().attribute("messages", messages);
}
} else {
AlertMessage[] messages = {new AlertMessage("Invalid password", AlertType.DANGER)};
request.session().attribute("messages", messages);
}
} else {
AlertMessage[] messages = {new AlertMessage("Passwords did not match", AlertType.DANGER)};
request.session().attribute("messages", messages);
}
response.redirect("/account");
return null;
}
}

View File

@ -0,0 +1,21 @@
package net.simon987.server.web;
import spark.ModelAndView;
import spark.Request;
import spark.Response;
import spark.TemplateViewRoute;
import java.util.HashMap;
import java.util.Map;
public class HomePage implements TemplateViewRoute {
@Override
public ModelAndView handle(Request request, Response response) throws Exception {
Map<String, Object> model = new HashMap<>(1);
model.put("session", request.session());
return new ModelAndView(model, "home.vm");
}
}

View File

@ -0,0 +1,20 @@
package net.simon987.server.web;
import spark.ModelAndView;
import spark.Request;
import spark.Response;
import spark.TemplateViewRoute;
import java.util.HashMap;
import java.util.Map;
public class LeaderBoardPage implements TemplateViewRoute {
@Override
public ModelAndView handle(Request request, Response response) {
Map<String, Object> model = new HashMap<>(1);
model.put("session", request.session());
return new ModelAndView(model, "leaderboard.vm");
}
}

View File

@ -0,0 +1,32 @@
package net.simon987.server.web;
import net.simon987.server.GameServer;
import net.simon987.server.logging.LogManager;
import spark.ModelAndView;
import spark.Request;
import spark.Response;
import spark.Route;
public class LoginRoute implements Route {
@Override
public ModelAndView handle(Request request, Response response) {
String username = request.queryParams("username");
String password = request.queryParams("password");
if (username != null && password != null) {
if (GameServer.INSTANCE.getUserManager().validateUser(username, password)) {
AlertMessage[] messages = {new AlertMessage("Logged in as " + username, AlertType.SUCCESS)};
request.session().attribute("messages", messages);
request.session().attribute("username", username);
LogManager.LOGGER.fine("(Web) " + username + " logged in");
} else {
AlertMessage[] messages = {new AlertMessage("Invalid username or password", AlertType.DANGER)};
request.session().attribute("messages", messages);
}
}
response.redirect("/account");
return null;
}
}

View File

@ -0,0 +1,18 @@
package net.simon987.server.web;
import spark.ModelAndView;
import spark.Request;
import spark.Response;
import spark.Route;
public class LogoutRoute implements Route {
@Override
public ModelAndView handle(Request request, Response response) {
AlertMessage[] messages = {new AlertMessage("Logged out", AlertType.INFO)};
request.session().attribute("messages", messages);
request.session().removeAttribute("username");
response.redirect("/account");
return null;
}
}

View File

@ -0,0 +1,20 @@
package net.simon987.server.web;
import spark.ModelAndView;
import spark.Request;
import spark.Response;
import spark.TemplateViewRoute;
import java.util.HashMap;
import java.util.Map;
public class PlayPage implements TemplateViewRoute {
@Override
public ModelAndView handle(Request request, Response response) {
Map<String, Object> model = new HashMap<>(1);
model.put("session", request.session());
return new ModelAndView(model, "play.vm");
}
}

View File

@ -0,0 +1,36 @@
package net.simon987.server.web;
import net.simon987.server.GameServer;
import net.simon987.server.logging.LogManager;
import net.simon987.server.user.RegistrationException;
import spark.ModelAndView;
import spark.Request;
import spark.Response;
import spark.Route;
public class RegisterRoute implements Route {
@Override
public ModelAndView handle(Request request, Response response) {
String username = request.queryParams("username");
String password = request.queryParams("password");
if (username != null && password != null) {
try {
GameServer.INSTANCE.getUserManager().registerUser(username, password);
AlertMessage[] messages = {new AlertMessage("Successfully registered", AlertType.SUCCESS)};
request.session().attribute("messages", messages);
request.session().attribute("username", username);
LogManager.LOGGER.fine("(Web) " + username + " registered " + request.ip());
} catch (RegistrationException e) {
AlertMessage[] messages = {new AlertMessage(e.getMessage(), AlertType.DANGER)};
request.session().attribute("messages", messages);
}
}
response.redirect("/account");
return null;
}
}

View File

@ -0,0 +1,64 @@
package net.simon987.server.web;
import net.simon987.server.GameServer;
import org.json.simple.JSONObject;
import spark.Request;
import spark.Response;
import spark.Route;
public class ServerInfoRoute implements Route {
private String address;
private String serverName;
private int tickLength;
public ServerInfoRoute() {
//Info variables that don't change
if (GameServer.INSTANCE.getConfig().getInt("use_ssl") == 0) {
address = "ws://" +
GameServer.INSTANCE.getConfig().getString("mar_address") + ":" +
GameServer.INSTANCE.getConfig().getString("mar_port") + "/socket";
} else {
address = "wss://" +
GameServer.INSTANCE.getConfig().getString("mar_address") + ":" +
GameServer.INSTANCE.getConfig().getString("mar_port") + "/socket";
}
serverName = GameServer.INSTANCE.getConfig().getString("server_name");
tickLength = GameServer.INSTANCE.getConfig().getInt("tick_length");
}
@Override
public Object handle(Request request, Response response) {
JSONObject json = new JSONObject();
String username = request.session().attribute("username");
if (username != null) {
String token = GameServer.INSTANCE.getUserManager().generateAndGetToken(username);
json.put("token", token);
json.put("username", username);
} else {
json.put("token",
"00000000000000000000000000000000" +
"00000000000000000000000000000000" +
"00000000000000000000000000000000" +
"00000000000000000000000000000000");
json.put("username", "guest");
}
json.put("address", address);
json.put("serverName", serverName);
json.put("tickLength", tickLength);
response.header("Content-Type", "application/json");
return json.toJSONString();
}
}

View File

@ -0,0 +1,54 @@
package net.simon987.server.web;
import net.simon987.server.ServerConfiguration;
import net.simon987.server.logging.LogManager;
import net.simon987.server.websocket.SocketServer;
import org.apache.velocity.app.VelocityEngine;
import spark.Spark;
import spark.template.velocity.VelocityTemplateEngine;
import java.util.Properties;
public class WebServer {
public WebServer(ServerConfiguration config) {
//Velocity config
Properties properties = new Properties();
properties.setProperty("file.resource.loader.path", "templates/");
VelocityTemplateEngine templateEngine = new VelocityTemplateEngine(new VelocityEngine(properties));
Spark.staticFiles.externalLocation("static");
//Spark config
if (config.getInt("use_ssl") != 0) {
/*
* Generate keystore from Let's Encrypt with command:
* openssl pkcs12 -export -in fullchain.pem -inkey privkey.pem -out keystore.kjs -name muchassemblyrequired -CAfile chain.pem -caname root -password file:password.txt
*
* Certificates generated from Let's Encrypt are usually in /etc/letsencrypt/live/www.site.com
*/
Spark.secure(
config.getString("keyStore_path"),
config.getString("keyStore_password"), null, null);
LogManager.LOGGER.info("(Web) Enabled ssl");
}
Spark.webSocket("/socket", SocketServer.class);
Spark.get("/", new HomePage(), templateEngine);
Spark.get("/leaderboard", new LeaderBoardPage(), templateEngine);
Spark.get("/play", new PlayPage(), templateEngine);
Spark.get("/account", new AccountPage(), templateEngine);
Spark.post("/register", new RegisterRoute());
Spark.post("/login", new LoginRoute());
Spark.get("/logout", new LogoutRoute());
Spark.post("/change_password", new ChangePasswordRoute());
Spark.get("/server_info", new ServerInfoRoute());
Spark.after((request, response) -> response.header("Content-Encoding", "gzip"));
}
}

View File

@ -36,13 +36,13 @@ public class SocketServer {
@OnWebSocketConnect @OnWebSocketConnect
public void onOpen(Session session) { public void onOpen(Session session) {
LogManager.LOGGER.info("(WS) New Websocket connection " + session.getRemoteAddress()); LogManager.LOGGER.info("(WS) New Websocket connection " + session.getRemoteAddress().getAddress());
onlineUserManager.add(new OnlineUser(session)); onlineUserManager.add(new OnlineUser(session));
} }
@OnWebSocketClose @OnWebSocketClose
public void onClose(Session session, int code, String reason) { public void onClose(Session session, int code, String reason) {
LogManager.LOGGER.info("(WS) Closed " + session.getRemoteAddress() + " with exit code " + code + " additional info: " + reason); LogManager.LOGGER.info("(WS) Closed " + session.getRemoteAddress().getAddress() + " with exit code " + code + " additional info: " + reason);
onlineUserManager.remove(onlineUserManager.getUser(session)); onlineUserManager.remove(onlineUserManager.getUser(session));
} }
@ -58,8 +58,7 @@ public class SocketServer {
} else { } else {
LogManager.LOGGER.info("(WS) Received message from unauthenticated user " + session.getRemoteAddress()); LogManager.LOGGER.info("(WS) Received message from unauthenticated user " + session.getRemoteAddress().getAddress());
if (message.length() == 128) { if (message.length() == 128) {
User user = GameServer.INSTANCE.getUserManager().validateAuthToken(message); User user = GameServer.INSTANCE.getUserManager().validateAuthToken(message);
@ -85,7 +84,7 @@ public class SocketServer {
onlineUser.getUser().setGuest(true); onlineUser.getUser().setGuest(true);
LogManager.LOGGER.info("(WS) Created guest user " + LogManager.LOGGER.info("(WS) Created guest user " +
onlineUser.getUser().getUsername() + session.getRemoteAddress()); onlineUser.getUser().getUsername() + session.getRemoteAddress().getAddress());
try { try {
session.getRemote().sendString("{\"t\":\"auth\", \"m\":\"ok\"}"); session.getRemote().sendString("{\"t\":\"auth\", \"m\":\"ok\"}");
@ -158,6 +157,5 @@ public class SocketServer {
} }
} }
} }
} }
} }

View File

@ -1,29 +1,41 @@
# MySQL username
mysql_user=mar
# MySQL password/
mysql_pass=mar
# MySQL address
mysql_url=jdbc:mysql://localhost:3306/mar?useSSL=false
# File management # File management
save_interval=5 save_interval=5
log_limit=2000000 log_limit=2000000
log_count=10 log_count=10
use_ssl=0 #ssl
cert_path=certificates use_ssl=1
keyStore_path=certificates/keystore.jks
keyStore_password=mar
#Server
mar_port=4567 mar_port=4567
mar_address=localhost mar_address=localhost
server_name=Official MAR server server_name=Official MAR server
# ---------------------------------------------- #Database
mongo_dbname=mar_beta
# Length of a tick in ms #Biomass
biomass_yield=2
minBiomassCount=3
minBiomassRespawnCount=2
maxBiomassCount=10
maxBiomassRespawnCount=6
biomassEnergyValue=4000
biomassRespawnTime=64
biomassRespawnThreshold=1
#World generation
wg_centerPointCountMin=5
wg_centerPointCountMax=15
wg_wallPlainRatio=4
wg_minIronCount=0
wg_maxIronCount=2
wg_minCopperCount=0
wg_maxCopperCount=2
#CPU
tick_length=1000 tick_length=1000
# Default offset of the origin (starting point of code execution) in words
org_offset=512 org_offset=512
# Address of the stack bottom
stack_bottom=65536 stack_bottom=65536
# Size of the memory in words
memory_size=65536 memory_size=65536
# Initial location of new user's controlled unit user_timeout=100
#User creation
new_user_worldX = 32767 new_user_worldX = 32767
new_user_worldY = 32767 new_user_worldY = 32767
new_user_dimension=w- new_user_dimension=w-
@ -33,64 +45,20 @@ new_user_code=; Welcome to Much Assembly required!\n\
.text\n\ .text\n\
\t; Write code here\n\ \t; Write code here\n\
\tbrk \tbrk
# Default held item
new_user_item=0 new_user_item=0
# ---------------------------------------------- #Cubot
# Biomass units for each blob
biomass_yield=2
# Minimum biomass blob count for the WorldGenerator
minBiomassCount=3
minBiomassRespawnCount=2
# Maximum biomass blob count for the WorldGenerator
maxBiomassCount=10
maxBiomassRespawnCount=6
# Energy generated by consuming biomass
biomassEnergyValue=4000
# Maximum energy of the battery hardware in kJ
battery_max_energy=60000 battery_max_energy=60000
# Maximum shield power
cubot_max_shield=100 cubot_max_shield=100
cubot_max_hp=250 cubot_max_hp=250
# Energy cost per unit to charge shield
shield_energy_cost=50 shield_energy_cost=50
# Time for biomass respawn in ticks #NPCs
biomassRespawnTime=64
# Respawn timer will start when biomass count is below this number
biomassRespawnThreshold=1
# NPC lifetime in ticks
npc_lifetime=1024 npc_lifetime=1024
# Maximum travel distance from the Factory in Worlds
npc_max_factory_distance=3 npc_max_factory_distance=3
# Maximum NPC per Factory
factory_max_npc_count=16 factory_max_npc_count=16
# Harvester max hp
harvester_hp_max=100 harvester_hp_max=100
# Harvester hp regeneration per tick
harvester_regen=5 harvester_regen=5
# Number of biomass units dropped on death
harvester_biomass_drop_count=8 harvester_biomass_drop_count=8
# ---------------------------------------------- #Vaults
# Minimum center point count for the WorldGenerator
wg_centerPointCountMin=5
# Maximum center point count for the WorldGenerator
wg_centerPointCountMax=15
# Wall/Plain tile ratio for the WorldGenerator
wg_wallPlainRatio=4
# Minimum iron tile count for the WorldGenerator
wg_minIronCount=0
# Maximum iron tile count for the WorldGenerator
wg_maxIronCount=2
# Minimum copper tile count for the WorldGenerator
wg_minCopperCount=0
# Maximum copper tile count for the WorldGenerator
wg_maxCopperCount=2
# ----------------------------------------------
# Maximum execution time of user code in ms
user_timeout=100
# ----------------------------------------------
vault_door_open_time=4 vault_door_open_time=4
electric_box_hp=250 electric_box_hp=250
min_electric_box_count=1 min_electric_box_count=1

View File

@ -49,7 +49,7 @@
</div> </div>
</div> </div>
<a class="btn btn-primary btn-lg btn-shadow text-mono" href="#" role="button">Play now!</a> <a class="btn btn-primary btn-lg btn-shadow text-mono" href="/play" role="button">Play now!</a>
</div> </div>