API improvements

This commit is contained in:
kuroppoi 2021-07-09 01:51:20 +02:00
parent 02a8211a5f
commit 34f45f1fad
19 changed files with 553 additions and 246 deletions

View file

@ -1,42 +1,36 @@
package brainwine.api;
import java.io.File;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.fasterxml.jackson.databind.ObjectMapper;
import brainwine.api.models.NewsEntry;
import brainwine.api.config.ApiConfig;
import brainwine.api.config.NewsEntry;
import brainwine.shared.JsonHelper;
public class Api {
private static final Logger logger = LogManager.getLogger();
private final List<NewsEntry> news = new ArrayList<>();
private final PropertyFile properties;
private final int gatewayPort;
private final int portalPort;
private final String hostAddress;
private final int hostPort;
private final ApiConfig config;
private final DataFetcher dataFetcher;
private final GatewayService gatewayService;
private final PortalService portalService;
public Api() {
this(new DefaultDataFetcher());
}
public Api(DataFetcher dataFetcher) {
long startTime = System.currentTimeMillis();
logger.info("Starting API ...");
loadNews();
logger.info("Loading properties ...");
properties = new PropertyFile(new File("api.properties"));
gatewayPort = properties.getInt("gateway_port", 5001);
portalPort = properties.getInt("portal_port", 5003);
hostAddress = properties.getString("gameserver_address", "127.0.0.1");
hostPort = properties.getInt("gameserver_port", 5002);
gatewayService = new GatewayService(this, gatewayPort);
portalService = new PortalService(portalPort);
this.dataFetcher = dataFetcher;
logger.info("Using data fetcher {}", dataFetcher.getClass().getName());
logger.info("Loading configuration ...");
config = loadConfig();
gatewayService = new GatewayService(this, config.getGatewayPort());
portalService = new PortalService(this, config.getPortalPort());
logger.info("All done! API startup took {} milliseconds", System.currentTimeMillis() - startTime);
}
@ -46,32 +40,34 @@ public class Api {
portalService.stop();
}
private void loadNews() {
logger.info("Loading news data ...");
news.clear();
File newsFile = new File("news.json");
private ApiConfig loadConfig() {
try {
ObjectMapper mapper = new ObjectMapper();
File file = new File("api.json");
if(!newsFile.exists()) {
logger.info("Generating default news file ...");
NewsEntry defaultNews = new NewsEntry("Default News", "This news entry was automatically generated.\nEdit 'news.json' to make your own!", "A more civilised age...");
mapper.writerWithDefaultPrettyPrinter().writeValue(newsFile, Arrays.asList(defaultNews));
if(!file.exists()) {
file.createNewFile();
JsonHelper.writeValue(file, ApiConfig.DEFAULT_CONFIG);
return ApiConfig.DEFAULT_CONFIG;
}
news.addAll(mapper.readerForListOf(NewsEntry.class).readValue(newsFile));
Collections.reverse(news); // Reverse the list so that the last article in the file gets shown first.
return JsonHelper.readValue(file, ApiConfig.class);
} catch (Exception e) {
logger.error("Failed to load news data", e);
logger.fatal("Failed to load configuration", e);
System.exit(-1);
}
return ApiConfig.DEFAULT_CONFIG;
}
public List<NewsEntry> getNews() {
return news;
return config.getNews();
}
public String getGameServerHost() {
return hostAddress + ":" + hostPort;
return config.getGameServerIp() + ":" + config.getGameServerPort();
}
public DataFetcher getDataFetcher() {
return dataFetcher;
}
}

View file

@ -0,0 +1,15 @@
package brainwine.api;
import java.util.Collection;
import brainwine.api.models.ZoneInfo;
public interface DataFetcher {
public boolean isPlayerNameTaken(String name);
public String registerPlayer(String name);
public String login(String name, String password);
public boolean verifyAuthToken(String name, String token);
public boolean verifyApiToken(String apiToken);
public Collection<ZoneInfo> fetchZoneInfo();
}

View file

@ -0,0 +1,40 @@
package brainwine.api;
import java.util.Collection;
import brainwine.api.models.ZoneInfo;
public class DefaultDataFetcher implements DataFetcher {
private static final UnsupportedOperationException exception = new UnsupportedOperationException("DefaultDataFetcher behavior is undefined.");
@Override
public boolean isPlayerNameTaken(String name) {
throw exception;
}
@Override
public String registerPlayer(String name) {
throw exception;
}
@Override
public String login(String name, String password) {
throw exception;
}
@Override
public boolean verifyAuthToken(String name, String token) {
throw exception;
}
@Override
public boolean verifyApiToken(String apiToken) {
throw exception;
}
@Override
public Collection<ZoneInfo> fetchZoneInfo() {
throw exception;
}
}

View file

@ -1,17 +1,15 @@
package brainwine.api;
import java.util.HashMap;
import java.util.Map;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import brainwine.api.models.PlayersRequest;
import brainwine.api.models.ServerConnectInfo;
import brainwine.api.models.SessionsRequest;
import brainwine.api.util.ContextUtils;
import brainwine.gameserver.GameServer;
import brainwine.gameserver.entity.player.PlayerManager;
import brainwine.api.handlers.NewsRequestHandler;
import brainwine.api.handlers.PasswordResetHandler;
import brainwine.api.handlers.PasswordForgotHandler;
import brainwine.api.handlers.PlayerLoginHandler;
import brainwine.api.handlers.PlayerRegistrationHandler;
import brainwine.api.handlers.RwcPurchaseHandler;
import brainwine.api.handlers.SimpleExceptionHandler;
import io.javalin.Javalin;
public class GatewayService {
@ -21,75 +19,16 @@ public class GatewayService {
public GatewayService(Api api, int port) {
logger.info("Starting GatewayService @ port {} ...", port);
PlayerManager playerManager = GameServer.getInstance().getPlayerManager();
DataFetcher dataFetcher = api.getDataFetcher();
String gameServerHost = api.getGameServerHost();
gateway = Javalin.create().start(port);
gateway.exception(Exception.class, (e, ctx) -> {
ContextUtils.error(ctx, "%s", e);
logger.error("Exception caught", e);
});
// News
gateway.get("/clients", ctx ->{
Map<String, Object> json = new HashMap<>();
json.put("posts", api.getNews());
ctx.json(json);
});
// Registration
gateway.post("/players", ctx -> {
PlayersRequest request = ctx.bodyValidator(PlayersRequest.class).get();
String name = request.getName();
if(playerManager.getPlayer(name) != null) {
ContextUtils.error(ctx, "Sorry, this username has already been taken.");
return;
}
String token = playerManager.register(name);
ctx.json(new ServerConnectInfo(api.getGameServerHost(), name, token));
});
// Login
gateway.post("/sessions", ctx -> {
SessionsRequest request = ctx.bodyValidator(SessionsRequest.class).get();
String name = request.getName();
String password = request.getPassword();
String token = request.getToken();
if(password != null) {
token = playerManager.login(name, password);
if(token == null) {
ContextUtils.error(ctx, "Username or password is incorrect. Please check your credentials.");
return;
}
} else if(token != null) {
if(!playerManager.verifyAuthToken(name, token)) {
ContextUtils.error(ctx, "The provided session token is invalid or has expired. Please try relogging.");
return;
}
} else {
ContextUtils.error(ctx, "No credentials provided.");
return;
}
ctx.json(new ServerConnectInfo(api.getGameServerHost(), name, token));
});
// Password reset request
gateway.post("/passwords/request", ctx -> {
ContextUtils.error(ctx, "Sorry, this feature is not implemented yet.");
});
// Password reset token entry
gateway.post("/passwords/reset", ctx -> {
ContextUtils.error(ctx, "Sorry, this feature is not implemented yet.");
});
// RWC purchases
gateway.post("/purchases", ctx -> {
ContextUtils.error(ctx, "Sorry, purchases with RWC are disabled.");
});
gateway.exception(Exception.class, new SimpleExceptionHandler());
gateway.get("/clients", new NewsRequestHandler(api.getNews()));
gateway.post("/players", new PlayerRegistrationHandler(dataFetcher, gameServerHost));
gateway.post("/sessions", new PlayerLoginHandler(dataFetcher, gameServerHost));
gateway.post("/passwords/request", new PasswordForgotHandler());
gateway.post("/passwords/reset", new PasswordResetHandler());
gateway.post("/purchases", new RwcPurchaseHandler());
}
public void stop() {

View file

@ -1,88 +1,29 @@
package brainwine.api;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import brainwine.gameserver.GameServer;
import brainwine.gameserver.zone.Zone;
import brainwine.api.handlers.SimpleExceptionHandler;
import brainwine.api.handlers.ZoneSearchHandler;
import io.javalin.Javalin;
import io.javalin.http.Context;
/**
* aka Zone Searcher
*/
public class PortalService {
public static final int PAGE_SIZE = 6;
private static final Logger logger = LogManager.getLogger();
private final Javalin portal;
public PortalService(int port) {
public PortalService(Api api, int port) {
logger.info("Starting PortalService @ port {} ...", port);
DataFetcher dataFetcher = api.getDataFetcher();
portal = Javalin.create().start(port);
portal.get("/v1/worlds", ctx -> {
List<Zone> zones = new ArrayList<>();
zones.addAll(GameServer.getInstance().getZoneManager().getZones());
List<Map<String, Object>> zoneInfoList = new ArrayList<>();
int page = 1;
// Filtering
if(hasQueryParam(ctx, "page")) {
page = Integer.parseInt(ctx.queryParam("page"));
}
if(hasQueryParam(ctx, "biome")) {
String value = ctx.queryParam("biome");
zones = filterZones(zones, zone -> zone.getBiome().toString().equalsIgnoreCase(value));
}
if(hasQueryParam(ctx, "name")) {
String value = ctx.queryParam("name");
zones = filterZones(zones, zone -> zone.getName().toLowerCase().contains(value.toLowerCase()));
}
if(hasQueryParam(ctx, "sort")) {
String value = ctx.queryParam("sort");
switch(value) {
case "popularity":
zones = filterZones(zones, zone -> zone.getPlayers().size() > 0);
break;
default:
break;
}
}
// Page
int fromIndex = (page - 1) * PAGE_SIZE;
int toIndex = page * PAGE_SIZE;
zones = zones.subList(fromIndex < 0 ? 0 : fromIndex > zones.size() ? zones.size() : fromIndex, toIndex > zones.size() ? zones.size() : toIndex);
// Compile info
for(Zone zone : zones) {
zoneInfoList.add(zone.getPortalConfig());
}
ctx.json(zoneInfoList);
});
portal.exception(Exception.class, new SimpleExceptionHandler());
portal.get("/v1/worlds", new ZoneSearchHandler(dataFetcher));
}
public void stop() {
portal.stop();
}
private boolean hasQueryParam(Context ctx, String param) {
return ctx.queryParam(param) != null;
}
private List<Zone> filterZones(Collection<Zone> zones, Predicate<? super Zone> predicate){
return zones.stream().filter(predicate).collect(Collectors.toList());
}
}

View file

@ -1,67 +0,0 @@
package brainwine.api;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.util.Properties;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class PropertyFile {
private static final Logger logger = LogManager.getLogger();
private final Properties properties = new Properties();
private final File file;
public PropertyFile(File file) {
this.file = file;
if(file.exists()) {
try {
properties.load(new FileInputStream(file));
} catch (Exception e) {
logger.error("Could not load {}", file, e);
save();
}
} else {
save();
}
}
public void save() {
try {
properties.store(new FileOutputStream(file), "Here you can change the server connection information.");
} catch(Exception e) {
logger.error("Could not save {}", file, e);
}
}
public void setProperty(String key, Object value) {
properties.setProperty(key, "" + value);
}
public String getString(String key, String def) {
if(!properties.containsKey(key)) {
properties.setProperty(key, def);
save();
return def;
}
return properties.getProperty(key);
}
public int getInt(String key, int def) {
try {
return Integer.parseInt(getString(key, "" + def));
} catch(NumberFormatException e) {
properties.setProperty(key, "" + def);
save();
return def;
}
}
public boolean getBoolean(String key, boolean def) {
return Boolean.parseBoolean(getString(key, "" + def));
}
}

View file

@ -0,0 +1,49 @@
package brainwine.api.config;
import java.beans.ConstructorProperties;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
@JsonIgnoreProperties(ignoreUnknown = true)
public class ApiConfig {
public static final ApiConfig DEFAULT_CONFIG = new ApiConfig("127.0.0.1", 5002, 5001, 5003, Arrays.asList(NewsEntry.DEFAULT_NEWS));
private final String gameServerIp;
private final int gameServerPort;
private final int gatewayPort;
private final int portalPort;
private final List<NewsEntry> news;
@ConstructorProperties({"game_server_ip", "game_server_port", "gateway_port", "portal_port", "news"})
public ApiConfig(String gameServerIp, int gameServerPort, int gatewayPort, int portalPort, List<NewsEntry> news) {
this.gameServerIp = gameServerIp;
this.gameServerPort = gameServerPort;
this.gatewayPort = gatewayPort;
this.portalPort = portalPort;
this.news = news;
Collections.reverse(this.news);
}
public String getGameServerIp() {
return gameServerIp;
}
public int getGameServerPort() {
return gameServerPort;
}
public int getGatewayPort() {
return gatewayPort;
}
public int getPortalPort() {
return portalPort;
}
public List<NewsEntry> getNews() {
return news;
}
}

View file

@ -1,11 +1,16 @@
package brainwine.api.models;
package brainwine.api.config;
import java.beans.ConstructorProperties;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
@JsonIgnoreProperties(ignoreUnknown = true)
public class NewsEntry {
public static final NewsEntry DEFAULT_NEWS = new NewsEntry("Default News",
"This news entry was automatically generated.\nEdit 'api.json' to make your own!",
"A long time ago...");
private final String title;
private final String content;
private final String date;

View file

@ -0,0 +1,23 @@
package brainwine.api.handlers;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import brainwine.api.config.NewsEntry;
import io.javalin.http.Context;
import io.javalin.http.Handler;
public class NewsRequestHandler implements Handler {
private final Map<String, Object> news = new HashMap<>();
public NewsRequestHandler(Collection<NewsEntry> posts) {
news.put("posts", posts);
}
@Override
public void handle(Context ctx) throws Exception {
ctx.json(news);
}
}

View file

@ -0,0 +1,14 @@
package brainwine.api.handlers;
import static brainwine.api.util.ContextUtils.*;
import io.javalin.http.Context;
import io.javalin.http.Handler;
public class PasswordForgotHandler implements Handler {
@Override
public void handle(Context ctx) throws Exception {
error(ctx, "Sorry, it is currently not possible to reset your password.");
}
}

View file

@ -0,0 +1,14 @@
package brainwine.api.handlers;
import static brainwine.api.util.ContextUtils.*;
import io.javalin.http.Context;
import io.javalin.http.Handler;
public class PasswordResetHandler implements Handler {
@Override
public void handle(Context ctx) throws Exception {
error(ctx, "Sorry, it is currently not possible to reset your password.");
}
}

View file

@ -0,0 +1,47 @@
package brainwine.api.handlers;
import static brainwine.api.util.ContextUtils.*;
import brainwine.api.DataFetcher;
import brainwine.api.models.ServerConnectInfo;
import brainwine.api.models.SessionsRequest;
import io.javalin.http.Context;
import io.javalin.http.Handler;
public class PlayerLoginHandler implements Handler {
private final DataFetcher dataFetcher;
private final String gameServerHost;
public PlayerLoginHandler(DataFetcher dataFetcher, String gameServerHost) {
this.dataFetcher = dataFetcher;
this.gameServerHost = gameServerHost;
}
@Override
public void handle(Context ctx) throws Exception {
SessionsRequest request = ctx.bodyValidator(SessionsRequest.class).get();
String name = request.getName();
String password = request.getPassword();
String token = request.getToken();
if(password != null) {
token = dataFetcher.login(name, password);
if(token == null) {
error(ctx, "Username or password is incorrect. Please check your credentials.");
return;
}
} else if(token != null) {
if(!dataFetcher.verifyAuthToken(name, token)) {
error(ctx, "The provided session token is invalid or has expired. Please try relogging.");
return;
}
} else {
error(ctx, "No credentials provided.");
return;
}
ctx.json(new ServerConnectInfo(gameServerHost, name, token));
}
}

View file

@ -0,0 +1,34 @@
package brainwine.api.handlers;
import static brainwine.api.util.ContextUtils.*;
import brainwine.api.DataFetcher;
import brainwine.api.models.PlayersRequest;
import brainwine.api.models.ServerConnectInfo;
import io.javalin.http.Context;
import io.javalin.http.Handler;
public class PlayerRegistrationHandler implements Handler {
private final DataFetcher dataFetcher;
private final String gameServerHost;
public PlayerRegistrationHandler(DataFetcher dataFetcher, String gameServerHost) {
this.dataFetcher = dataFetcher;
this.gameServerHost = gameServerHost;
}
@Override
public void handle(Context ctx) throws Exception {
PlayersRequest request = ctx.bodyValidator(PlayersRequest.class).get();
String name = request.getName();
if(dataFetcher.isPlayerNameTaken(name)) {
error(ctx, "Sorry, this username has already been taken.");
return;
}
String token = dataFetcher.registerPlayer(name);
ctx.json(new ServerConnectInfo(gameServerHost, name, token));
}
}

View file

@ -0,0 +1,14 @@
package brainwine.api.handlers;
import static brainwine.api.util.ContextUtils.*;
import io.javalin.http.Context;
import io.javalin.http.Handler;
public class RwcPurchaseHandler implements Handler {
@Override
public void handle(Context ctx) throws Exception {
error(ctx, "Sorry, RWC purchases are disabled.");
}
}

View file

@ -0,0 +1,20 @@
package brainwine.api.handlers;
import static brainwine.api.util.ContextUtils.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import io.javalin.http.Context;
import io.javalin.http.ExceptionHandler;
public class SimpleExceptionHandler implements ExceptionHandler<Exception> {
private static final Logger logger = LogManager.getLogger();
@Override
public void handle(Exception exception, Context ctx) {
logger.error("Exception caught", exception);
error(ctx, "%s", exception);
}
}

View file

@ -0,0 +1,77 @@
package brainwine.api.handlers;
import static brainwine.api.util.ContextUtils.*;
import java.util.List;
import brainwine.api.DataFetcher;
import brainwine.api.models.ZoneInfo;
import io.javalin.http.Context;
import io.javalin.http.Handler;
public class ZoneSearchHandler implements Handler {
public static final int PAGE_SIZE = 6;
private final DataFetcher dataFetcher;
public ZoneSearchHandler(DataFetcher dataFetcher) {
this.dataFetcher = dataFetcher;
}
@Override
public void handle(Context ctx) throws Exception {
final List<ZoneInfo> zones = (List<ZoneInfo>)dataFetcher.fetchZoneInfo();
String apiToken = ctx.queryParam("api_token");
if(apiToken == null || !dataFetcher.verifyApiToken(apiToken)) {
error(ctx, "A valid api token is required for this request.");
return;
}
handleQueryParam(ctx, "name", String.class, name -> {
zones.removeIf(zone -> !zone.getName().toLowerCase().contains(name.toLowerCase()));
});
handleQueryParam(ctx, "activity", String.class, activity -> {
zones.removeIf(zone -> zone.getActivity() == null || !zone.getActivity().equalsIgnoreCase(activity));
});
handleQueryParam(ctx, "biome", String.class, biome -> {
zones.removeIf(zone -> !zone.getBiome().equalsIgnoreCase(biome));
});
handleQueryParam(ctx, "pvp", boolean.class, pvp -> {
zones.removeIf(zone -> zone.isPvp() != pvp);
});
handleQueryParam(ctx, "protected", boolean.class, locked -> {
zones.removeIf(zone -> zone.isLocked() != locked);
});
handleQueryParam(ctx, "residency", String.class, residency -> {
zones.clear(); // not supported yet
});
handleQueryParam(ctx, "account", String.class, account -> {
zones.clear(); // not supported yet
});
handleQueryParam(ctx, "sort", String.class, sort -> {
switch(sort) {
case "popularity":
zones.removeIf(zone -> zone.getPlayerCount() == 0);
zones.sort((a, b) -> Integer.compare(b.getPlayerCount(), a.getPlayerCount()));
break;
case "created":
break;
}
});
// Page
int page = ctx.queryParam("page", Integer.class, "1").get();
int fromIndex = (page - 1) * PAGE_SIZE;
int toIndex = page * PAGE_SIZE;
ctx.json(zones.subList(fromIndex < 0 ? 0 : fromIndex > zones.size() ? zones.size() : fromIndex, toIndex > zones.size() ? zones.size() : toIndex));
}
}

View file

@ -0,0 +1,68 @@
package brainwine.api.models;
import com.fasterxml.jackson.annotation.JsonProperty;
public class ZoneInfo {
private final String name;
private final String biome;
private final String activity;
private final boolean pvp;
private final boolean premium;
private final boolean locked;
private final int playerCount;
private final double explorationProgress;
private final String generationDate;
public ZoneInfo(String name, String biome, String activity, boolean pvp, boolean premium, boolean locked, int playerCount, double explorationProgress, String generationDate) {
this.name = name;
this.biome = biome;
this.activity = activity;
this.pvp = pvp;
this.premium = premium;
this.locked = locked;
this.playerCount = playerCount;
this.explorationProgress = explorationProgress;
this.generationDate = generationDate;
}
public String getName() {
return name;
}
public String getBiome() {
return biome;
}
public String getActivity() {
return activity;
}
public boolean isPvp() {
return pvp;
}
public boolean isPremium() {
return premium;
}
@JsonProperty("protected")
public boolean isLocked() {
return locked;
}
@JsonProperty("players")
public int getPlayerCount() {
return playerCount;
}
@JsonProperty("explored")
public double getExplorationProgress() {
return explorationProgress;
}
@JsonProperty("gen_date")
public String getGenerationDate() {
return generationDate;
}
}

View file

@ -2,7 +2,9 @@ package brainwine.api.util;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Consumer;
import io.javalin.core.validation.Validator;
import io.javalin.http.Context;
public class ContextUtils {
@ -12,4 +14,13 @@ public class ContextUtils {
map.put("error", String.format(message, args));
ctx.json(map);
}
public static <T> void handleQueryParam(Context ctx, String key, Class<T> type, Consumer<T> handler) {
Validator<T> param = ctx.queryParam(key, type);
T value = param.getOrNull();
if(value != null) {
handler.accept(value);
}
}
}

View file

@ -0,0 +1,67 @@
package brainwine;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import brainwine.api.DataFetcher;
import brainwine.api.models.ZoneInfo;
import brainwine.gameserver.entity.player.PlayerManager;
import brainwine.gameserver.zone.Zone;
import brainwine.gameserver.zone.ZoneManager;
public class DirectDataFetcher implements DataFetcher {
private final PlayerManager playerManager;
private final ZoneManager zoneManager;
public DirectDataFetcher(PlayerManager playerManager, ZoneManager zoneManager) {
this.playerManager = playerManager;
this.zoneManager = zoneManager;
}
@Override
public boolean isPlayerNameTaken(String name) {
return playerManager.getPlayer(name) != null;
}
@Override
public String registerPlayer(String name) {
return playerManager.register(name);
}
@Override
public String login(String name, String password) {
return playerManager.login(name, password);
}
@Override
public boolean verifyAuthToken(String name, String token) {
return playerManager.verifyAuthToken(name, token);
}
@Override
public boolean verifyApiToken(String apiToken) {
return true; // TODO
}
@Override
public Collection<ZoneInfo> fetchZoneInfo() {
List<ZoneInfo> zoneInfo = new ArrayList<>();
Collection<Zone> zones = zoneManager.getZones();
for(Zone zone : zones) {
zoneInfo.add(new ZoneInfo(zone.getName(),
zone.getBiome().getId(),
null,
false,
false,
false,
zone.getPlayers().size(),
zone.getExplorationProgress(),
"2021-02-15"));
}
return zoneInfo;
}
}