diff --git a/assets/resources/icons.png b/assets/resources/icons.png index e9c5eaf6..6a357080 100644 Binary files a/assets/resources/icons.png and b/assets/resources/icons.png differ diff --git a/assets/resources/localization/english_en-us.mcpl b/assets/resources/localization/english_en-us.mcpl index ea6b249e..b461d3d4 100644 --- a/assets/resources/localization/english_en-us.mcpl +++ b/assets/resources/localization/english_en-us.mcpl @@ -1264,11 +1264,15 @@ Yes No No -# PlayerInvMenu.java +# PlayerInvDisplay.java Inventory Inventory +to search. +to search. + + # TitleMenu.java Singleplayer diff --git a/assets/resources/logo.png b/assets/resources/logo.png index 79cc5cc7..8e263db4 100644 Binary files a/assets/resources/logo.png and b/assets/resources/logo.png differ diff --git a/assets/resources/textures/gui.png b/assets/resources/textures/gui.png index f9cba129..85ce8783 100644 Binary files a/assets/resources/textures/gui.png and b/assets/resources/textures/gui.png differ diff --git a/assets/resources/textures/icons.png b/assets/resources/textures/icons.png index 12dc9b36..f08c768f 100644 Binary files a/assets/resources/textures/icons.png and b/assets/resources/textures/icons.png differ diff --git a/assets/resources/textures/legacy/gui_legacy.png b/assets/resources/textures/legacy/gui_legacy.png index 9073ab28..e240fa4f 100644 Binary files a/assets/resources/textures/legacy/gui_legacy.png and b/assets/resources/textures/legacy/gui_legacy.png differ diff --git a/assets/resources/textures/legacy/items_legacy.png b/assets/resources/textures/legacy/items_legacy.png index d0967c10..685f9719 100644 Binary files a/assets/resources/textures/legacy/items_legacy.png and b/assets/resources/textures/legacy/items_legacy.png differ diff --git a/assets/resources/textures/legacy/tiles_legacy.png b/assets/resources/textures/legacy/tiles_legacy.png index 41f2f402..02c6f661 100644 Binary files a/assets/resources/textures/legacy/tiles_legacy.png and b/assets/resources/textures/legacy/tiles_legacy.png differ diff --git a/assets/resources/textures/tiles.png b/assets/resources/textures/tiles.png index 55b60461..fb82dca7 100644 Binary files a/assets/resources/textures/tiles.png and b/assets/resources/textures/tiles.png differ diff --git a/src/Minicraft_Splash_Screen_1.png b/src/Minicraft_Splash_Screen_1.png index 6c20ac50..f8b886f3 100644 Binary files a/src/Minicraft_Splash_Screen_1.png and b/src/Minicraft_Splash_Screen_1.png differ diff --git a/src/Minicraft_Splash_Screen_2.png b/src/Minicraft_Splash_Screen_2.png index 66a254c5..5d77167b 100644 Binary files a/src/Minicraft_Splash_Screen_2.png and b/src/Minicraft_Splash_Screen_2.png differ diff --git a/src/Minicraft_Splash_Screen_3.png b/src/Minicraft_Splash_Screen_3.png index 8737a42c..5e5ffe46 100644 Binary files a/src/Minicraft_Splash_Screen_3.png and b/src/Minicraft_Splash_Screen_3.png differ diff --git a/src/minicraft/core/Renderer.java b/src/minicraft/core/Renderer.java index 991c808f..8f35cdf4 100644 --- a/src/minicraft/core/Renderer.java +++ b/src/minicraft/core/Renderer.java @@ -240,9 +240,9 @@ private static void renderGui() { } if (isMode("creative") || ac >= 10000) { - Font.drawCompleteBackground(" x" + "∞", screen, 184 - player.activeItem.arrAdjusted, Screen.h - 24); + Font.drawTransparentBackground(" x" + "∞", screen, 184 - player.activeItem.arrAdjusted, Screen.h - 24); } else { - Font.drawCompleteBackground(" x" + ac, screen, 184 - player.activeItem.arrAdjusted, Screen.h - 24); + Font.drawTransparentBackground(" x" + ac, screen, 184 - player.activeItem.arrAdjusted, Screen.h - 24); } // Displays the arrow icon @@ -286,7 +286,7 @@ private static void renderGui() { screen.render(xx + x * 8, yy, 3 + 21 * 32, 0, 3); } - Font.drawCompleteBackground(dura + "%", screen, 220 + player.activeItem.durAdjusted, Screen.h - 24, Color.get(1, 255 - green, green, 0)); + Font.drawTransparentBackground(dura + "%", screen, 220 + player.activeItem.durAdjusted, Screen.h - 24, Color.get(1, 255 - green, green, 0)); } // This draws the black square where the selected item would be if you were holding it diff --git a/src/minicraft/core/io/Localization.java b/src/minicraft/core/io/Localization.java index 0f609594..3903b931 100644 --- a/src/minicraft/core/io/Localization.java +++ b/src/minicraft/core/io/Localization.java @@ -187,7 +187,12 @@ private static String[] getLanguagesFromDirectoryUsingIDE() { DirectoryStream dir = Files.newDirectoryStream(folderPath); for (Path p : dir) { String filename = p.getFileName().toString(); - languages.add(filename.replace(".mcpl", "")); + String data = filename.replace(".mcpl", ""); + String lang = data.substring(0, data.indexOf('_')); + + languages.add(lang); + localizationFiles.put(lang, "/resources/localization/"+filename); + locales.put(lang, Locale.forLanguageTag(data.substring(data.indexOf('_') + 1))); } } catch (IOException | URISyntaxException e) { e.printStackTrace(); diff --git a/src/minicraft/core/io/SoundOGG.java b/src/minicraft/core/io/SoundOGG.java deleted file mode 100644 index 36704e34..00000000 --- a/src/minicraft/core/io/SoundOGG.java +++ /dev/null @@ -1,62 +0,0 @@ -package minicraft.core.io; - -import java.io.IOException; - -import org.newdawn.easyogg.OggClip; - -public class SoundOGG { - // Ogg support! ;) - - // Player - public static final SoundOGG Theme_Surface = new SoundOGG("/resources/sound/Background/Surface.OGG"); - - OggClip ogg; - - private SoundOGG(String filename) { - try { - ogg = new OggClip(filename); - } catch (IOException e) { - e.printStackTrace(); - } - } - - boolean flipMute = true; - - public void toggleSound() { - try { - if (flipMute) { - ogg.stop(); - } else { - ogg.loop(); - } - flipMute = !flipMute; - } catch (Exception e) { - e.printStackTrace(); - } - } - - public void play() { - try { - ogg.loop(); - } catch (Exception e) { - e.printStackTrace(); - } - } - - public void pause() { - try { - ogg.stop(); - } catch (Exception e) { - e.printStackTrace(); - } - } - - public void close() { - try { - ogg.stop(); - ogg.close(); - } catch (Exception e) { - e.printStackTrace(); - } - } -} diff --git a/src/minicraft/entity/furniture/DeathChest.java b/src/minicraft/entity/furniture/DeathChest.java index dc65ed21..2b5558d0 100644 --- a/src/minicraft/entity/furniture/DeathChest.java +++ b/src/minicraft/entity/furniture/DeathChest.java @@ -93,19 +93,20 @@ public boolean use(Player player) { public void take(Player player) { } // can't grab a death chest. - @Override - public void touchedBy(Entity other) { - if (other instanceof Player) { - if (!Game.ISONLINE) { - ((Player) other).getInventory().addAll(getInventory()); - remove(); - Game.notifications.add("Death chest retrieved!"); - } else if (Game.isValidClient()) { - Game.client.touchDeathChest((Player) other, this); - remove(); - } - } - } + @Override + public void touchedBy(Entity other) { + if(other instanceof Player) { + if(!Game.ISONLINE) { + ((Player)other).getInventory().addAll(getInventory()); + remove(); + Game.notifications.add("Death chest retrieved!"); + } + else if(Game.isValidClient()) { + Game.client.touchDeathChest(this); + remove(); + } + } + } @Override protected String getUpdateString() { diff --git a/src/minicraft/entity/furniture/Furniture.java b/src/minicraft/entity/furniture/Furniture.java index 384d612d..1834af3b 100644 --- a/src/minicraft/entity/furniture/Furniture.java +++ b/src/minicraft/entity/furniture/Furniture.java @@ -128,20 +128,19 @@ public boolean interact(Player player, @Nullable Item item, Direction attackDir) return false; } - /** - * Tries to let the player push this furniture. - * - * @param player The player doing the pushing. - */ - public void tryPush(Player player) { - if (pushTime == 0) { - pushDir = player.dir; // set pushDir to the player's dir. - pushTime = multiPushTime = 10; // set pushTime to 10. - - if (Game.isConnectedClient()) - Game.client.pushFurniture(this, pushDir); - } - } + /** + * Tries to let the player push this furniture. + * @param player The player doing the pushing. + */ + public void tryPush(Player player) { + if (pushTime == 0) { + pushDir = player.dir; // Set pushDir to the player's dir. + pushTime = multiPushTime = 10; // Set pushTime to 10. + + if (Game.isConnectedClient()) + Game.client.pushFurniture(this); + } + } @Override public boolean canWool() { diff --git a/src/minicraft/entity/furniture/Tnt.java b/src/minicraft/entity/furniture/Tnt.java index 098b6ab9..d1d6648c 100644 --- a/src/minicraft/entity/furniture/Tnt.java +++ b/src/minicraft/entity/furniture/Tnt.java @@ -17,6 +17,7 @@ import minicraft.gfx.Sprite; import minicraft.item.Item; import minicraft.level.Level; +import minicraft.level.tile.Tile; import minicraft.level.tile.Tiles; public class Tnt extends Furniture implements ActionListener { @@ -29,7 +30,7 @@ public class Tnt extends Furniture implements ActionListener { private Timer explodeTimer; private Level levelSave; - private String[] explosionBlacklist = new String[] { "hard rock", "obsidian wall", "hard obsidian" }; + private final String[] explosionBlacklist = new String[] { "hard rock", "obsidian wall", "hard obsidian", "stairs up", "stairs down" }; /** * Creates a new tnt furniture. @@ -58,7 +59,9 @@ public void tick() { float dist = (float) Math.hypot(e.x - x, e.y - y); int dmg = (int) (BLAST_DAMAGE * (1 - (dist / BLAST_RADIUS))) + 1; if (e instanceof Mob) - ((Mob) e).hurt(this, dmg); + ((Mob)e).onExploded(this, dmg); + + // Ignite other bombs in range. if (e instanceof Tnt) { Tnt tnt = (Tnt) e; if (!tnt.fuseLit) { @@ -69,6 +72,18 @@ public void tick() { } } + int xt = x >> 4; + int yt = (y - 2) >> 4; + + // Get the tiles that have been exploded. + Tile[] affectedTiles = level.getAreaTiles(xt, yt, 1); + + // Call the onExplode() event. + for (int i = 0; i < affectedTiles.length; i++) { + // This assumes that range is 1. + affectedTiles[i].onExplode(level, xt + i % 3 - 1, yt + i / 3 - 1); + } + // Random explode sound switch (random.nextInt(4)) { @@ -98,9 +113,6 @@ public void tick() { } - int xt = x >> 4; - int yt = (y - 2) >> 4; - level.setAreaTiles(xt, yt, 1, Tiles.get("explode"), 0, explosionBlacklist); levelSave = level; diff --git a/src/minicraft/entity/mob/Keeper.java b/src/minicraft/entity/mob/Keeper.java index a1fa7908..91a6da8e 100644 --- a/src/minicraft/entity/mob/Keeper.java +++ b/src/minicraft/entity/mob/Keeper.java @@ -85,7 +85,7 @@ public void render(Screen screen) { String txt = ""; int w = Font.textWidth(txt) / 2; - Font.drawCompleteBackground(txt, screen, x - w, y - 45 - Font.textHeight()); + Font.drawTransparentBackground(txt, screen, x - w, y - 45 - Font.textHeight()); } public boolean canSwim() { diff --git a/src/minicraft/entity/mob/Mob.java b/src/minicraft/entity/mob/Mob.java index 25410076..1d54baf6 100644 --- a/src/minicraft/entity/mob/Mob.java +++ b/src/minicraft/entity/mob/Mob.java @@ -217,9 +217,14 @@ public void hurt(Mob mob, int damage, Direction attackDir) { // Hurt the mob, wh doHurt(damage, attackDir); // Call the method that actually performs damage, and use our provided attackDir } - public void hurt(Tnt tnt, int dmg) { - doHurt(dmg, getAttackDir(tnt, this)); - } + /** + * Executed when a TNT bomb explodes near this mob. + * @param tnt The TNT exploding. + * @param dmg The amount of damage the explosion does. + */ + public void onExploded(Tnt tnt, int dmg) { + doHurt(dmg, getAttackDir(tnt, this)); + } protected void doHurt(int damage, Direction attackDir) { // Actually hurt the mob, based on only damage and a // direction diff --git a/src/minicraft/entity/mob/MobAi.java b/src/minicraft/entity/mob/MobAi.java index 6624bf7a..93b3efbd 100644 --- a/src/minicraft/entity/mob/MobAi.java +++ b/src/minicraft/entity/mob/MobAi.java @@ -3,6 +3,7 @@ import minicraft.core.Game; import minicraft.core.io.Sound; import minicraft.entity.Direction; +import minicraft.entity.Entity; import minicraft.entity.particle.TextParticle; import minicraft.gfx.Color; import minicraft.gfx.MobSprite; @@ -62,8 +63,12 @@ public void tick() { if (lifetime > 0) { age++; if (age > lifetime) { - remove(); - return; + boolean playerClose = getLevel().entityNearPlayer((Entity) this); + + if (!playerClose) { + remove(); + return; + } } } diff --git a/src/minicraft/entity/mob/Player.java b/src/minicraft/entity/mob/Player.java index d8663599..33da4a6e 100644 --- a/src/minicraft/entity/mob/Player.java +++ b/src/minicraft/entity/mob/Player.java @@ -1280,22 +1280,39 @@ public void findStartPos(Level level, boolean setSpawn) { this.y = spawnPos.y * 16 + 8; } - /** - * Finds a location where the player can respawn in a given level. - * - * @param level The level. - * @return true - */ - public boolean respawn(Level level) { - if (!level.getTile(spawnx, spawny).maySpawn()) - findStartPos(level); // If there's no bed to spawn from, and the stored coordinates don't point to a - // grass tile, then find a new point. - - // Move the player to the spawnpoint - this.x = spawnx * 16 + 8; - this.y = spawny * 16 + 8; - return true; // Again, why the "return true"'s for methods that never return false? - } + /** + * Finds a location where the player can respawn in a given level. + * + * @param level The level. + * @return true + */ + public void respawn(Level level) { + if (!level.getTile(spawnx, spawny).maySpawn()) { + findStartPos(level); // If there's no bed to spawn from, and the stored coordinates don't point to a grass tile, then find a new point. + + int x = spawnx; + int y = spawny; + + x = (Math.random() > .5) ? -80 : 80; + y = (Math.random() > .5) ? -80 : 80; + + for (int i = 0; i < 20; i++) // Iterate through diagonal line for possible random spawn. + { + if (!level.getTile(spawnx, spawny).maySpawn()) { + x += 4 * (x / -x); + y += 4 * (-y / y); + } else { + spawnx = x; + spawny = y; + break; + } + } + + } + // Move the player to the spawnpoint + this.x = spawnx * 16 + 8; + this.y = spawny * 16 + 8; + } /** * Uses an amount of stamina to do an action. @@ -1366,8 +1383,8 @@ else if (Game.isConnectedClient()) } @Override - public void hurt(Tnt tnt, int dmg) { - super.hurt(tnt, dmg); + public void onExploded(Tnt tnt, int dmg) { + super.onExploded(tnt, dmg); payStamina(dmg * 2); } diff --git a/src/minicraft/gfx/Font.java b/src/minicraft/gfx/Font.java index d159d097..a1398467 100644 --- a/src/minicraft/gfx/Font.java +++ b/src/minicraft/gfx/Font.java @@ -81,11 +81,11 @@ public static void drawBackground(String msg, Screen screen, int x, int y, int w } } - public static void drawCompleteBackground(String msg, Screen screen, int x, int y) { - drawCompleteBackground(msg, screen, x, y, -1); + public static void drawTransparentBackground(String msg, Screen screen, int x, int y) { + drawTransparentBackground(msg, screen, x, y, -1); } - public static void drawCompleteBackground(String msg, Screen screen, int x, int y, int whiteTint) { + public static void drawTransparentBackground(String msg, Screen screen, int x, int y, int whiteTint) { msg = msg.toUpperCase(Localization.getSelectedLocale()); for (int i = 0; i < msg.length(); i++) { diff --git a/src/minicraft/item/Item.java b/src/minicraft/item/Item.java index 13681b57..d4fb29cd 100644 --- a/src/minicraft/item/Item.java +++ b/src/minicraft/item/Item.java @@ -150,7 +150,7 @@ public void renderHUD(Screen screen, int x, int y, int fontColor) { sprite.render(screen, xx, yy); // Item name - Font.drawCompleteBackground(dispName, screen, xx + 8, yy, fontColor); + Font.drawTransparentBackground(dispName, screen, xx + 8, yy, fontColor); } diff --git a/src/minicraft/level/Level.java b/src/minicraft/level/Level.java index 72e6beb4..f43536e5 100644 --- a/src/minicraft/level/Level.java +++ b/src/minicraft/level/Level.java @@ -12,7 +12,6 @@ import java.util.Random; import java.util.Set; import java.util.function.Predicate; -import java.util.function.ToIntFunction; // Game imports import minicraft.core.Game; @@ -128,21 +127,16 @@ public static String getDepthString(int depth) { * sparks set */ private final Object entityLock = new Object(); - private Set < Entity > entities = java.util.Collections.synchronizedSet(new HashSet < > ()); // A list of all the entities in the world - private Set < Spark > sparks = java.util.Collections.synchronizedSet(new HashSet < > ()); // A list of all the sparks in the world - private Set < Spark2 > sparks2 = java.util.Collections.synchronizedSet(new HashSet < > ()); // A list of all the sparks2 in the world - private Set < Spark3 > sparks3 = java.util.Collections.synchronizedSet(new HashSet < > ()); // A list of all the sparks3 in the world - private Set < Player > players = java.util.Collections.synchronizedSet(new HashSet < > ()); // A list of all the players in the world - private List < Entity > entitiesToAdd = new ArrayList < > (); // entities that will be added to the level on next tick, are stored here. This is for the sake of multithreading, optimization. (hopefully) - private List < Entity > entitiesToRemove = new ArrayList < > (); // entities that will be removed from the level on next tick are stored here. This is for the sake of multithreading optimization. (hopefully) + private final Set < Entity > entities = java.util.Collections.synchronizedSet(new HashSet < > ()); // A list of all the entities in the world + private final Set < Spark > sparks = java.util.Collections.synchronizedSet(new HashSet < > ()); // A list of all the sparks in the world + private final Set < Spark2 > sparks2 = java.util.Collections.synchronizedSet(new HashSet < > ()); // A list of all the sparks2 in the world + private final Set < Spark3 > sparks3 = java.util.Collections.synchronizedSet(new HashSet < > ()); // A list of all the sparks3 in the world + private final Set < Player > players = java.util.Collections.synchronizedSet(new HashSet < > ()); // A list of all the players in the world + private final List < Entity > entitiesToAdd = new ArrayList < > (); // entities that will be added to the level on next tick, are stored here. This is for the sake of multithreading, optimization. (hopefully) + private final List < Entity > entitiesToRemove = new ArrayList < > (); // entities that will be removed from the level on next tick are stored here. This is for the sake of multithreading optimization. (hopefully) // creates a sorter for all the entities to be rendered. - private static Comparator < Entity > spriteSorter = Comparator.comparingInt(new ToIntFunction < Entity > () { - @Override - public int applyAsInt(Entity e) { - return e.y; - } - }); + private static Comparator spriteSorter = Comparator.comparingInt(e -> e.y); public Entity[] getEntitiesToSave() { Entity[] allEntities = new Entity[entities.size() + sparks.size() + sparks2.size() + sparks3.size() + entitiesToAdd.size()]; @@ -175,9 +169,9 @@ public void printTileLocs(Tile t) { printLevelLoc(t.name, x, y); } - public void printEntityLocs(Class < ? extends Entity> c) { + public void printEntityLocs(Class < ? extends Entity > c) { int numfound = 0; - for (Entity entity : getEntityArray()) { + for (Entity entity: getEntityArray()) { if (c.isAssignableFrom(entity.getClass())) { printLevelLoc(entity.toString(), entity.x >> 4, entity.y >> 4); numfound++; @@ -581,13 +575,7 @@ public void tick(boolean fullTick) { Entity removeThis = (Entity) entities.toArray()[(random.nextInt(entities.size()))]; if (removeThis instanceof MobAi) { // make sure there aren't any close players - boolean playerClose = false; - for (Player player: players) { - if (Math.abs(player.x - removeThis.x) < 128 && Math.abs(player.y - removeThis.x) < 76) { - playerClose = true; - break; - } - } + boolean playerClose = entityNearPlayer(removeThis); if (!playerClose) { remove(removeThis); @@ -638,6 +626,16 @@ public void tick(boolean fullTick) { trySpawn(); } + + + public boolean entityNearPlayer(Entity entity) { + for (Player player : players) { + if (Math.abs(player.x - entity.x) < 128 && Math.abs(player.y - entity.y) < 76) { + return true; + } + } + return false; + } /* * public void printEntityStatus(String entityMessage, Entity entity, String... @@ -1051,10 +1049,9 @@ public Entity[] getEntityArray() { public List < Entity > getEntitiesInTiles(int xt, int yt, int radius) { return getEntitiesInTiles(xt, yt, radius, false); } - + @SafeVarargs - public final List getEntitiesInTiles(int xt, int yt, int radius, boolean includeGiven, - Class < ? extends Entity>... entityClasses) { + public final List < Entity > getEntitiesInTiles(int xt, int yt, int radius, boolean includeGiven, Class < ? extends Entity > ...entityClasses) { return getEntitiesInTiles(xt - radius, yt - radius, xt + radius, yt + radius, includeGiven, entityClasses); } diff --git a/src/minicraft/level/LevelGen.java b/src/minicraft/level/LevelGen.java index 6f09c143..0efdefe8 100644 --- a/src/minicraft/level/LevelGen.java +++ b/src/minicraft/level/LevelGen.java @@ -36,9 +36,9 @@ private LevelGen(int w, int h, int featureSize) { /// to be 16 or 32, in the code below. for (int y = 0; y < w; y += featureSize) { for (int x = 0; x < w; x += featureSize) { - - // This method sets the random value from -1 to 1 at the given coordinate. - setSample(x, y, random.nextFloat() * 2 - 1); + + // This method sets the random value from -1 to 1 at the given coordinate. + setSample(x, y, random.nextFloat() * 2 - 1); } } @@ -52,12 +52,12 @@ private LevelGen(int w, int h, int featureSize) { for (int x = 0; x < w; x += stepSize) { // this loops through the values again, by a given increment... double a = sample(x, y); // fetches the value at the coordinate set previously (it fetches the exact - // same ones that were just set above) + // same ones that were just set above) double b = sample(x + stepSize, y); // fetches the value at the next coordinate over. This could - // possibly loop over at the end, and fetch the first value in - // the row instead. + // possibly loop over at the end, and fetch the first value in + // the row instead. double c = sample(x, y + stepSize); // fetches the next value down, possibly looping back to the top - // of the column. + // of the column. double d = sample(x + stepSize, y + stepSize); // fetches the value one down, one right. /* @@ -110,19 +110,19 @@ private LevelGen(int w, int h, int featureSize) { // surrounding mids. double H = (a + b + d + e) / 4.0 + (random.nextFloat() * 2 - 1) * stepSize * scale * 0.5; // adds - // middle, - // right, - // mr-mb, - // mr-mt, - // and - // random. + // middle, + // right, + // mr-mb, + // mr-mt, + // and + // random. double g = (a + c + d + f) / 4.0 + (random.nextFloat() * 2 - 1) * stepSize * scale * 0.5; // adds - // middle, - // bottom, - // mr-mb, - // ml-mb, - // and - // random. + // middle, + // bottom, + // mr-mb, + // ml-mb, + // and + // random. setSample(x + halfStep, y, H); // Sets the H to the mid-right setSample(x, y + halfStep, g); // Sets the g to the mid-bottom @@ -178,7 +178,6 @@ static byte[][] createAndValidateMap(int w, int h, int level) { return createAndValidateTopMap(w, h); if (level == -4) return createAndValidateDungeon(w, h); - if ((level > -4) && (level < 0)) return createAndValidateUndergroundMap(w, h, -level); @@ -199,18 +198,12 @@ private static byte[][] createAndValidateTopMap(int w, int h) { count[result[0][i] & 0xff]++; } - if (count[Tiles.get("rock").id & 0xff] < 100) - continue; - if (count[Tiles.get("sand").id & 0xff] < 100) - continue; - if (count[Tiles.get("grass").id & 0xff] < 100) - continue; - if (count[Tiles.get("tree").id & 0xff] < 100) - continue; - if (count[Tiles.get("flower").id & 0xff] < 100) - continue; - if (count[Tiles.get("Stairs Down").id & 0xff] < w / 21) - continue; // size 128 = 6 stairs min + if (count[Tiles.get("rock").id & 0xff] < 100) continue; + if (count[Tiles.get("sand").id & 0xff] < 100) continue; + if (count[Tiles.get("grass").id & 0xff] < 100) continue; + if (count[Tiles.get("tree").id & 0xff] < 100) continue; + if (count[Tiles.get("flower").id & 0xff] < 100) continue; + if (count[Tiles.get("Stairs Down").id & 0xff] < w / 21) continue; // size 128 = 6 stairs min return result; @@ -229,15 +222,11 @@ private static byte[][] createAndValidateUndergroundMap(int w, int h, int depth) count[result[0][i] & 0xff]++; } - if (count[Tiles.get("rock").id & 0xff] < 100) - continue; - if (count[Tiles.get("dirt").id & 0xff] < 100) - continue; - if (count[(Tiles.get("iron Ore").id & 0xff) + depth - 1] < 20) - continue; + if (count[Tiles.get("rock").id & 0xff] < 100) continue; + if (count[Tiles.get("dirt").id & 0xff] < 100) continue; + if (count[(Tiles.get("iron Ore").id & 0xff) + depth - 1] < 20) continue; - if (depth < 3 && count[Tiles.get("Stairs Down").id & 0xff] < w / 32) - continue; // size 128 = 4 stairs min + if (depth < 3 && count[Tiles.get("Stairs Down").id & 0xff] < w / 32) continue; // size 128 = 4 stairs min return result; @@ -256,12 +245,9 @@ private static byte[][] createAndValidateDungeon(int w, int h) { count[result[0][i] & 0xff]++; } - if (count[Tiles.get("Obsidian").id & 0xff] < 100) - continue; - if (count[Tiles.get("Obsidian Wall").id & 0xff] < 100) - continue; - if (count[Tiles.get("Hard obsidian").id & 0xff] < 100) - continue; + if (count[Tiles.get("Obsidian").id & 0xff] < 100) continue; + if (count[Tiles.get("Obsidian Wall").id & 0xff] < 100) continue; + if (count[Tiles.get("Hard obsidian").id & 0xff] < 100) continue; return result; @@ -280,10 +266,8 @@ private static byte[][] createAndValidateSkyMap(int w, int h) { count[result[0][i] & 0xff]++; } - if (count[Tiles.get("cloud").id & 0xff] < 2000) - continue; - if (count[Tiles.get("Stairs Down").id & 0xff] < w / 64) - continue; // size 128 = 2 stairs min + if (count[Tiles.get("cloud").id & 0xff] < 2000) continue; + if (count[Tiles.get("Stairs Down").id & 0xff] < w / 64) continue; // size 128 = 2 stairs min return result; @@ -331,72 +315,87 @@ private static byte[][] createTopMap(int w, int h) { // create surface map // Code of the type of terrain, this according to the user's option switch ((String) Settings.get("Type")) { - case "Island": - if (val < -0.5) { - if (Settings.get("Theme").equals("Hell")) { - map[i] = Tiles.get("lava").id; - } else { - map[i] = Tiles.get("water").id; - } + case "Island": - } else if (val > 0.5 && mval < -1.5) { - map[i] = Tiles.get("rock").id; - } else { - map[i] = Tiles.get("grass").id; - } + if (val < -0.5) { + if (Settings.get("Theme").equals("Hell")) { + map[i] = Tiles.get("lava").id; + } else { + map[i] = Tiles.get("water").id; + } - break; - case "Box": + } else if (val > 0.5 && mval < -1.5) { + + map[i] = Tiles.get("up rock").id; + + } else if (val > 0.1 && mval < -1.1) { + + map[i] = Tiles.get("rock").id; - if (val < -1.5) { - if (Settings.get("Theme").equals("Hell")) { - map[i] = Tiles.get("lava").id; } else { - map[i] = Tiles.get("water").id; + map[i] = Tiles.get("grass").id; + } - } else if (val > 0.5 && mval < -1.5) { - map[i] = Tiles.get("rock").id; - } else { - map[i] = Tiles.get("grass").id; - } + break; + case "Box": - break; - case "Mountain": + if (val < -1.5) { + if (Settings.get("Theme").equals("Hell")) { + map[i] = Tiles.get("lava").id; + } else { + map[i] = Tiles.get("water").id; + } + + } else if (val > 0.5 && mval < -1.5) { + + map[i] = Tiles.get("up rock").id; + + } else if (val > 0.1 && mval < -1.1) { + + map[i] = Tiles.get("rock").id; - if (val < -0.4) { - map[i] = Tiles.get("grass").id; - } else if (val > 0.5 && mval < -1.5) { - if (Settings.get("Theme").equals("Hell")) { - map[i] = Tiles.get("lava").id; } else { - map[i] = Tiles.get("water").id; + map[i] = Tiles.get("grass").id; + } - } else { - map[i] = Tiles.get("rock").id; - } - break; - case "Irregular": + break; + case "Mountain": - if (val < -0.5 && mval < -0.5) { - if (Settings.get("Theme").equals("Hell")) { - map[i] = Tiles.get("lava").id; - } - if (!Settings.get("Theme").equals("Hell")) { - map[i] = Tiles.get("water").id; + if (val < -0.4) { + map[i] = Tiles.get("grass").id; + } else if (val > 0.5 && mval < -1.5) { + if (Settings.get("Theme").equals("Hell")) { + map[i] = Tiles.get("lava").id; + } else { + map[i] = Tiles.get("water").id; + } + } else { + map[i] = Tiles.get("rock").id; } - } else if (val > 0.5 && mval < -1.5) { - map[i] = Tiles.get("rock").id; - } else { - map[i] = Tiles.get("grass").id; - } - break; + break; + case "Irregular": - default: - // meh - break; + if (val < -0.5 && mval < -0.5) { + if (Settings.get("Theme").equals("Hell")) { + map[i] = Tiles.get("lava").id; + } + if (!Settings.get("Theme").equals("Hell")) { + map[i] = Tiles.get("water").id; + } + + } else if (val > 0.5 && mval < -1.5) { + map[i] = Tiles.get("rock").id; + } else { + map[i] = Tiles.get("grass").id; + } + break; + + default: + // meh + break; } } } @@ -408,7 +407,7 @@ private static byte[][] createTopMap(int w, int h) { // create surface map for (int i = 0; i < w * h / 800; i++) { int xs = random.nextInt(w); int ys = random.nextInt(h); - for (int k = 0; k < 10; k++) { + for (int k = 0; k < 16; k++) { int x = xs + random.nextInt(29) - 10 + random.nextInt(5); int y = ys + random.nextInt(29) - 10 + random.nextInt(5); for (int j = 0; j < 100; j++) { @@ -503,6 +502,7 @@ private static byte[][] createTopMap(int w, int h) { // create surface map } } + // Add trees to biomes // Classic forest biome @@ -700,13 +700,14 @@ private static byte[][] createTopMap(int w, int h) { // create surface map if (xx >= 0 && yy >= 0 && xx < w && yy < h) { if (map[xx + yy * w] == Tiles.get("grass").id) { map[xx + yy * w] = Tiles.get("flower").id; - data[xx + yy * w] = (byte) (col + random.nextInt(4) * 16); // data determines which way the - // flower faces + data[xx + yy * w] = (byte)(col + random.nextInt(4) * 16); // data determines which way the + // flower faces } } } } + // Add lawn to grass for (int i = 0; i < w * h / 400; i++) { int x = random.nextInt(w); @@ -718,7 +719,7 @@ private static byte[][] createTopMap(int w, int h) { // create surface map if (xx >= 0 && yy >= 0 && xx < w && yy < h) { if (map[xx + yy * w] == Tiles.get("grass").id) { map[xx + yy * w] = Tiles.get("lawn").id; - data[xx + yy * w] = (byte) (col + random.nextInt(4) * 16); + data[xx + yy * w] = (byte)(col + random.nextInt(4) * 16); } } } @@ -735,7 +736,7 @@ private static byte[][] createTopMap(int w, int h) { // create surface map if (xx >= 0 && yy >= 0 && xx < w && yy < h) { if (map[xx + yy * w] == Tiles.get("grass").id) { map[xx + yy * w] = Tiles.get("orange tulip").id; - data[xx + yy * w] = (byte) (col + random.nextInt(4) * 16); + data[xx + yy * w] = (byte)(col + random.nextInt(4) * 16); } } } @@ -824,12 +825,17 @@ private static byte[][] createTopMap(int w, int h) { // create surface map } } + + // System.out.println("min="+min); // System.out.println("max="+max); // average /= w*h; // System.out.println(average); - return new byte[][] { map, data }; + return new byte[][] { + map, + data + }; } // Dungeons generation code @@ -896,7 +902,10 @@ private static byte[][] createDungeon(int w, int h) { } } - return new byte[][] { map, data }; + return new byte[][] { + map, + data + }; } // Generate cave system @@ -979,7 +988,7 @@ else if (depth == 1) int yy = y + random.nextInt(5) - random.nextInt(5); if (xx >= r && yy >= r && xx < w - r && yy < h - r) { if (map[xx + yy * w] == Tiles.get("rock").id) { - map[xx + yy * w] = (byte) ((Tiles.get("iron Ore").id & 0xff) + depth - 1); + map[xx + yy * w] = (byte)((Tiles.get("iron Ore").id & 0xff) + depth - 1); } } } @@ -990,7 +999,7 @@ else if (depth == 1) int yy = y + random.nextInt(3) - random.nextInt(2); if (xx >= r && yy >= r && xx < w - r && yy < h - r) { if (map[xx + yy * w] == Tiles.get("rock").id) { - map[xx + yy * w] = (byte) (Tiles.get("Lapis").id & 0xff); + map[xx + yy * w] = (byte)(Tiles.get("Lapis").id & 0xff); } } } @@ -1010,7 +1019,7 @@ else if (depth == 1) /// basically prevents negative values... except... this doesn't do anything if /// you flip it back to a byte again... - map[xx + yy * w] = (byte) (Tiles.get("Stairs Down").id & 0xff); + map[xx + yy * w] = (byte)(Tiles.get("Stairs Down").id & 0xff); } } } @@ -1041,7 +1050,10 @@ else if (depth == 1) } } - return new byte[][] { map, data }; + return new byte[][] { + map, + data + }; } // Sky dimension generation @@ -1217,7 +1229,7 @@ private static byte[][] createSkyMap(int w, int h) { if (xx >= 0 && yy >= 0 && xx < w && yy < h) { if (map[xx + yy * w] == Tiles.get("sky grass").id) { map[xx + yy * w] = Tiles.get("sky lawn").id; - data[xx + yy * w] = (byte) (col + random.nextInt(4) * 16); + data[xx + yy * w] = (byte)(col + random.nextInt(4) * 16); } } } @@ -1398,7 +1410,7 @@ private static byte[][] createSkyMap(int w, int h) { if (xx >= 0 && yy >= 0 && xx < w && yy < h) { if (map[xx + yy * w] == Tiles.get("sky grass").id) { map[xx + yy * w] = Tiles.get("sky lawn").id; - data[xx + yy * w] = (byte) (col + random.nextInt(4) * 16); + data[xx + yy * w] = (byte)(col + random.nextInt(4) * 16); } } } @@ -1489,7 +1501,10 @@ private static byte[][] createSkyMap(int w, int h) { } } - return new byte[][] { map, data }; + return new byte[][] { + map, + data + }; } @@ -1537,7 +1552,7 @@ public static void main(String[] args) { // Execute it forever // noinspection InfiniteLoopStatement - + boolean hasquit = false; while (!hasquit) { // stop the loop and close the program.) @@ -1574,14 +1589,14 @@ public static void main(String[] args) { for (int y = 0; y < h; y++) { // Loops through the height of the map for (int x = 0; x < w; x++) { // (inner-loop)Loops through the entire width of the map int i = x + y * w; // Current tile of the map. - - /*The colors used in the pixels are hexadecimal (0xRRGGBB). - 0xff0000 would be fully red - 0x00ff00 would be fully blue - 0x0000ff would be fully green - 0x000000 would be black - and 0xffffff would be white etc. - */ + + /*The colors used in the pixels are hexadecimal (0xRRGGBB). + 0xff0000 would be fully red + 0x00ff00 would be fully blue + 0x0000ff would be fully green + 0x000000 would be black + and 0xffffff would be white etc. + */ // Surface tiles if (map[i] == Tiles.get("water").id) pixels[i] = 0x1a2c89; @@ -1638,6 +1653,7 @@ public static void main(String[] args) { if (map[i] == Tiles.get("Sky lawn").id) pixels[i] = 0x56a383; if (map[i] == Tiles.get("Sky high grass").id) pixels[i] = 0x4f9678; if (map[i] == Tiles.get("Holy rock").id) pixels[i] = 0x7a7a7a; + if (map[i] == Tiles.get("Up rock").id) pixels[i] = 0x939393; } } @@ -1656,9 +1672,13 @@ public static void main(String[] args) { System.out.println("[LevelGen]" + " | " + "Seed: " + worldSeed + " | " + "Gen-Version: " + Game.BUILD + " | " + finalGenTime); img.setRGB(0, 0, w, h, pixels, 0, w); // Sets the pixels into the image - - String[] options = {"Another", "Quit"}; // Name of the buttons used for the window. - + + String[] options = { + "Another", + "Quit" + }; // Name of the buttons used for the window. + + int Generator = JOptionPane.showOptionDialog(null, null, "Map Generator", JOptionPane.YES_NO_OPTION, JOptionPane.QUESTION_MESSAGE, new ImageIcon(img.getScaledInstance(w * mapScale, h * mapScale, Image.SCALE_AREA_AVERAGING)), options, null); if (LevelGen.worldSeed == 0x100) { @@ -1666,14 +1686,16 @@ public static void main(String[] args) { } else { LevelGen.worldSeed = 0x100; } - - /* Now you noticed that we made the dialog an integer. This is because when you click a button it will return a number. - Since we passed in 'options', the window will return 0 if you press "Another" and it will return 1 when you press "Quit". - If you press the red "x" close mark, the window will return -1 - */ - + + /* Now you noticed that we made the dialog an integer. This is because when you click a button it will return a number. + Since we passed in 'options', the window will return 0 if you press "Another" and it will return 1 when you press "Quit". + If you press the red "x" close mark, the window will return -1 + */ + // If the dialog returns -1 (red "x" button) or 1 ("Quit" button) then... - if (Generator == -1 || Generator == 1) hasquit = true; // Stop the loop and close the program. + if (Generator == -1 || Generator == 1) { + hasquit = true; // Stop the loop and close the program. + } } } } \ No newline at end of file diff --git a/src/minicraft/level/NoiseMap.java b/src/minicraft/level/NoiseMap.java index 52ad9cce..f99cb1c1 100644 --- a/src/minicraft/level/NoiseMap.java +++ b/src/minicraft/level/NoiseMap.java @@ -111,4 +111,4 @@ public static void main(String[] args) { JOptionPane.showMessageDialog(null, null, "Another", JOptionPane.YES_NO_OPTION, new ImageIcon(img)); } } -} +} \ No newline at end of file diff --git a/src/minicraft/level/tile/DirtTile.java b/src/minicraft/level/tile/DirtTile.java index 6741d9d0..85aa2fc4 100644 --- a/src/minicraft/level/tile/DirtTile.java +++ b/src/minicraft/level/tile/DirtTile.java @@ -27,6 +27,8 @@ protected DirtTile(String name) { protected static int dCol(int depth) { switch (depth) { + case 1: + return Color.get(1, 194, 194, 194); // Sky. case 0: return Color.get(1, 129, 105, 83); // surface. case -4: diff --git a/src/minicraft/level/tile/RockTile.java b/src/minicraft/level/tile/RockTile.java index d9ce336b..2da9c01f 100644 --- a/src/minicraft/level/tile/RockTile.java +++ b/src/minicraft/level/tile/RockTile.java @@ -23,7 +23,24 @@ public class RockTile extends Tile { private ConnectorSprite sprite = new ConnectorSprite(RockTile.class, new Sprite(18, 6, 3, 3, 1, 3), - new Sprite(21, 8, 2, 2, 1, 3), new Sprite(21, 6, 2, 2, 1, 3)); + new Sprite(21, 8, 2, 2, 1, 3), new Sprite(21, 6, 2, 2, 1, 3)) { + + + public boolean connectsTo(Tile tile, boolean isSide) { + return tile != Tiles.get("dirt") && tile != Tiles.get("grass") && + tile != Tiles.get("sand") && tile != Tiles.get("Orange tulip") && + tile != Tiles.get("tree") && tile != Tiles.get("birch tree") && + tile != Tiles.get("Stairs Down") && tile != Tiles.get("Stairs up") && + tile != Tiles.get("lava") && tile != Tiles.get("water") && + tile != Tiles.get("cactus") && tile != Tiles.get("flower") && + tile != Tiles.get("Hole") && tile != Tiles.get("Snow") && + tile != Tiles.get("Lawn") && tile != Tiles.get("path") && + tile != Tiles.get("Birch tree") && tile != Tiles.get("Fir tree") && + tile != Tiles.get("Wood wall") && tile != Tiles.get("path") && + tile != Tiles.get("ice spike") && tile != Tiles.get("Carrot") && + tile != Tiles.get("pine tree"); + } + }; private boolean dropCoal = false; private final int maxHealth = 50; diff --git a/src/minicraft/level/tile/Tile.java b/src/minicraft/level/tile/Tile.java index fe5e558d..8436ccf1 100644 --- a/src/minicraft/level/tile/Tile.java +++ b/src/minicraft/level/tile/Tile.java @@ -41,6 +41,7 @@ public ToolType getRequiredTool() { public boolean connectsToGrass = false; public boolean connectsToCloud = false; + public boolean connectsToUpRock = false; public boolean connectsToSand = false; public boolean connectsToFluid = false; protected boolean connectsToLava = false; @@ -57,6 +58,7 @@ public ToolType getRequiredTool() { protected boolean connectsToSkyDirt = false; public boolean connectsToSkyHighGrass = false; public boolean connectsToFerrosite = false; + public boolean connectsToDirt = false; { light = 1; @@ -149,6 +151,18 @@ public void steppedOn(Level level, int xt, int yt, Entity entity) { public boolean interact(Level level, int xt, int yt, Player player, Item item, Direction attackDir) { return false; } + + /** + * Executed when the tile is exploded. + * The call for this method is done just before the tiles are changed to exploded tiles. + * @param level The level we are on. + * @param xt X position of the tile. + * @param yt Y position of the tile. + * @return true if successful. + */ + public boolean onExplode(Level level, int xt, int yt) { + return false; + } /** Sees if the tile connects to a fluid. */ public boolean connectsToLiquid() { diff --git a/src/minicraft/level/tile/Tiles.java b/src/minicraft/level/tile/Tiles.java index d8ece4fb..73d5a406 100644 --- a/src/minicraft/level/tile/Tiles.java +++ b/src/minicraft/level/tile/Tiles.java @@ -130,6 +130,7 @@ public static void initTileList() { tiles.set(83, new GoldenCloudTreeTile("Golden Cloud Tree")); tiles.set(84, new BlueCloudTreeTile("Blue Cloud Tree")); tiles.set(85, new SkyFernTile("Sky Fern")); + tiles.set(86, new UpRockTile("Up rock")); // tiles.set(?, new SandRockTile("Sand rock")); diff --git a/src/minicraft/level/tile/UpRockTile.java b/src/minicraft/level/tile/UpRockTile.java new file mode 100644 index 00000000..70b303ea --- /dev/null +++ b/src/minicraft/level/tile/UpRockTile.java @@ -0,0 +1,134 @@ +package minicraft.level.tile; + +import minicraft.core.Game; +import minicraft.core.io.Settings; +import minicraft.core.io.Sound; +import minicraft.entity.Direction; +import minicraft.entity.Entity; +import minicraft.entity.mob.Mob; +import minicraft.entity.mob.Player; +import minicraft.entity.particle.SmashParticle; +import minicraft.entity.particle.TextParticle; +import minicraft.gfx.Color; +import minicraft.gfx.ConnectorSprite; +import minicraft.gfx.Screen; +import minicraft.gfx.Sprite; +import minicraft.item.Item; +import minicraft.item.Items; +import minicraft.item.ToolItem; +import minicraft.item.ToolType; +import minicraft.level.Level; + +public class UpRockTile extends Tile { + private ConnectorSprite sprite = new ConnectorSprite(UpRockTile.class, new Sprite(58, 6, 3, 3, 1, 3), + new Sprite(61, 8, 2, 2, 1, 3), new Sprite(61, 6, 2, 2, 1, 3)) { + + + public boolean connectsTo(Tile tile, boolean isSide) { + return tile != Tiles.get("rock") && tile != Tiles.get("dirt") && tile != Tiles.get("grass") && + tile != Tiles.get("sand") && tile != Tiles.get("Orange tulip") && + tile != Tiles.get("tree") && tile != Tiles.get("birch tree") && + tile != Tiles.get("Stairs Down") && tile != Tiles.get("Stairs up") && + tile != Tiles.get("lava") && tile != Tiles.get("water") && + tile != Tiles.get("cactus") && tile != Tiles.get("flower") && + tile != Tiles.get("Hole") && tile != Tiles.get("Snow") && + tile != Tiles.get("Lawn") && tile != Tiles.get("path") && + tile != Tiles.get("Birch tree") && tile != Tiles.get("Fir tree") && + tile != Tiles.get("Wood wall") && tile != Tiles.get("path") && + tile != Tiles.get("ice spike") && tile != Tiles.get("Carrot") ; + } + }; + + private boolean dropCoal = false; + private final int maxHealth = 50; + + private int damage; + + protected UpRockTile(String name) { + super(name, (ConnectorSprite) null); + csprite = sprite; + } + + @Override + public void render(Screen screen, Level level, int x, int y) { + + Tiles.get("rock").render(screen, level, x, y); + sprite.render(screen, level, x, y); + } + + @Override + public boolean mayPass(Level level, int x, int y, Entity e) { + return false; + } + + @Override + public boolean hurt(Level level, int x, int y, Mob source, int dmg, Direction attackDir) { + hurt(level, x, y, dmg); + return true; + } + + @Override + public boolean interact(Level level, int xt, int yt, Player player, Item item, Direction attackDir) { + + // creative mode can just act like survival here + if (item instanceof ToolItem) { + ToolItem tool = (ToolItem) item; + if (tool.type == ToolType.Pickaxe && player.payStamina(4 - tool.level) && tool.payDurability()) { + + // Drop coal since we use a pickaxe. + dropCoal = true; + hurt(level, xt, yt, random.nextInt(10) + (tool.level) * 5 + 10); + return true; + } + } + return false; + } + + @Override + public void hurt(Level level, int x, int y, int dmg) { + damage = level.getData(x, y) + dmg; + + if (Game.isMode("creative")) { + dmg = damage = maxHealth; + dropCoal = true; + } + + level.add(new SmashParticle(x * 16, y * 16)); + Sound.Tile_generic_hurt.play(); + + level.add(new TextParticle("" + dmg, x * 16 + 8, y * 16 + 8, Color.RED)); + if (damage >= maxHealth) { + + if (dropCoal) { + level.dropItem(x * 16 + 8, y * 16 + 8, 1, 3, Items.get("Stone")); + int coal = 0; + + if (!Settings.get("diff").equals("Hard")) { + coal++; + } + + level.dropItem(x * 16 + 8, y * 16 + 8, coal, coal + 1, Items.get("Coal")); + + } else { + level.dropItem(x * 16 + 8, y * 16 + 8, 2, 4, Items.get("Stone")); + + } + + level.setTile(x, y, Tiles.get("Rock")); + + } else { + level.setData(x, y, damage); + + } + } + + @Override + public boolean tick(Level level, int xt, int yt) { + damage = level.getData(xt, yt); + if (damage > 0) { + level.setData(xt, yt, damage - 1); + return true; + } + return false; + } + } \ No newline at end of file diff --git a/src/minicraft/network/MinicraftClient.java b/src/minicraft/network/MinicraftClient.java index ee06c0a0..71d92afe 100644 --- a/src/minicraft/network/MinicraftClient.java +++ b/src/minicraft/network/MinicraftClient.java @@ -44,738 +44,606 @@ import minicraft.screen.ContainerDisplay; import minicraft.screen.MultiplayerDisplay; -/// This class is only used by the client runtime; the server runtime doesn't touch it. +// This class is only used by the client runtime; the server runtime doesn't touch it. public class MinicraftClient extends MinicraftConnection { - - public static final int DEFAULT_CONNECT_TIMEOUT = 5_000; // in milliseconds - - private MultiplayerDisplay menu; - - private enum State { - LOGIN, LOADING, PLAY, RESPAWNING, DISCONNECTED - } - - private State curState = State.DISCONNECTED; - - private HashMap entityRequests = new HashMap<>(); - - private int serverPlayerCount = 0; - - @Nullable - private static Socket openSocket(String hostName, MultiplayerDisplay menu, int connectTimeout) { - InetAddress hostAddress; - Socket socket; - - socket = connectWithSrv(hostName, menu, connectTimeout); - if (socket != null) - return socket; - - if (Game.debug) - System.out.println("getting host address from host name \"" + hostName + "\"..."); - - // check if there's a custom port to connect on - int port = MinicraftProtocol.PORT; - String[] splitHostName = hostName.split(":"); - if (splitHostName.length > 1) { - hostName = splitHostName[0]; - try { - port = Integer.parseInt(splitHostName[1]); - } catch (NumberFormatException exception) { - System.err.println("Invalid port: " + splitHostName[1]); - menu.setError("Invalid port"); - exception.printStackTrace(); - return null; - } - } - - try { - hostAddress = InetAddress.getByName(hostName); - } catch (UnknownHostException ex) { - System.err.println("Don't know about host " + hostName); - menu.setError("host not found"); - ex.printStackTrace(); - return null; - } - - if (Game.debug) - System.out.println("host found. attempting to open socket..."); - - try { - socket = new Socket(); - socket.connect(new InetSocketAddress(hostAddress, port), connectTimeout); - } catch (IOException ex) { - System.err.println("Problem connecting socket to server:"); - menu.setError(ex.getMessage().replace(" (Connection refused)", "")); - ex.printStackTrace(); - return null; - } - - if (Game.debug) - System.out.println("successfully connected to game server. Returning socket..."); - - return socket; - } - - @Nullable - private static Socket connectWithSrv(String hostName, MultiplayerDisplay menu, int connectTimeout) { - // Perform an SRV lookup to determine if any exist - String query = "_minicraft._tcp." + hostName; - Record[] records; - - try { - records = new Lookup(query, Type.SRV).run(); - } catch (TextParseException e) { - if (Game.debug) - System.err.println("Error running SRV lookup on '" + hostName + "', skipping."); - return null; - } - - if (records == null) { - // no records - if (Game.debug) - System.out.println("No SRV records found."); - return null; - } - - // record-sorting queue - PriorityQueue queue = new PriorityQueue<>((rec1, rec2) -> { - // get smaller priority - int pComp = Integer.compare(rec1.getPriority(), rec2.getPriority()); - if (pComp != 0) - return pComp; - // get larger weight - int wComp = Integer.compare(rec2.getWeight(), rec1.getWeight()); - if (wComp != 0) - return wComp; - - // else equal - return 0; - }); - - // menu.setWaitMessage("Trying available servers"); - - // run through each record in the right order and try to connect - for (Record record : records) { - if (Game.debug) - System.out.println("Found SRV record: " + record.toString()); - SRVRecord srec = (SRVRecord) record; - if (Game.debug) { - System.out.println("SRV record data:"); - System.out.println("Target: '" + srec.getTarget() + "'"); - System.out.println("Port: '" + srec.getPort() + "'"); - System.out.println("Priority: '" + srec.getPriority() + "'"); - System.out.println("Weight: '" + srec.getWeight() + "'"); - } - - queue.add(srec); - } - - // attempt to connect to each record in order of their precedence - int serverIdx = 1; - while (!queue.isEmpty()) { - SRVRecord srec = queue.poll(); - - // remove trailing . - String address = srec.getTarget().toString(true); - int port = srec.getPort(); - - menu.setWaitMessage("Trying server " + serverIdx + "/" + records.length); - try { - Socket socket = new Socket(); - socket.connect(new InetSocketAddress(address, port), connectTimeout); - return socket; - } catch (IOException ex) { - System.err.println( - "Failed to connect to server " + serverIdx + "/" + records.length + ": " + ex.getMessage()); - } - serverIdx++; - } - - menu.setWaitMessage("Trying main server"); - - return null; - } - - public MinicraftClient(String username, MultiplayerDisplay menu, String hostName) { - this(username, menu, hostName, DEFAULT_CONNECT_TIMEOUT); - } - - public MinicraftClient(String username, MultiplayerDisplay menu, String hostName, int connectTimeout) { - super("MinicraftClient", openSocket(hostName, menu, connectTimeout)); - this.menu = menu; - Game.ISONLINE = true; - Game.ISHOST = false; - - if (super.isConnected()) { - login(username); - start(); - } - } - - public int getPlayerCount() { - return serverPlayerCount; - } - - @SuppressWarnings("incomplete-switch") - private void changeState(State newState) { - if (Game.debug) - System.out.println("CLIENT: client state change from " + curState + " to " + newState); - curState = newState; - - switch (newState) { - case LOGIN: - sendData(InputType.LOGIN, ((RemotePlayer) Game.player).getUsername() + ";" + Game.VERSION); - break; - - case LOADING: - Game.setMenu(menu); - menu.setLoadingMessage("Tiles"); - sendData(InputType.LOAD, String.valueOf(Game.currentLevel)); - break; - - case PLAY: - if (Game.debug) - System.out.println("CLIENT: Begin game!"); - World.levels[Game.currentLevel].add(Game.player); - Renderer.readyToRenderGameplay = true; - Game.setMenu(null); - break; - - case RESPAWNING: - Game.setMenu(menu); - menu.setLoadingMessage("Spawnpoint"); - sendData(InputType.RESPAWN, ""); - break; - } - } - - private void login(String username) { - if (Game.debug) - System.out.println("CLIENT: logging in to server..."); - - try { - Game.player = new RemotePlayer(Game.player, true, InetAddress.getLocalHost(), getConnectedPort()); - ((RemotePlayer) Game.player).setUsername(username); - } catch (UnknownHostException ex) { - System.err.println("CLIENT could not get localhost address:"); - ex.printStackTrace(); - menu.setError("unable to get localhost address"); - } - changeState(State.LOGIN); - } - - /** This method is responsible for parsing all data received by the socket. */ - @SuppressWarnings("incomplete-switch") - public boolean parsePacket(InputType inType, String alldata) { - String[] data = alldata.split(";"); - - if (Game.packet_debug) - System.out.println("Received:" + inType.toString() + ", " + alldata); - - switch (inType) { - case INVALID: - System.err.println("CLIENT received error: " + alldata); - menu.setError(alldata); - endConnection(); - return false; - - case PING: - // if(Game.debug) System.out.println("CLIENT: received server ping"); - sendData(InputType.PING, alldata); - return true; - - case LOGIN: - System.err.println("Server tried to login..."); - return false; - - case DISCONNECT: - if (Game.debug) - System.out.println("CLIENT: received disconnect"); - menu.setError("Server Disconnected."); // this sets the menu back to the multiplayer menu, and tells the - // user what happened. - endConnection(); - return true; - - case GAME: - Settings.set("mode", data[0]); - Updater.setTime(Integer.parseInt(data[1])); - Updater.gamespeed = Float.parseFloat(data[2]); - Updater.pastDay1 = Boolean.parseBoolean(data[3]); - Updater.scoreTime = Integer.parseInt(data[4]); - serverPlayerCount = Integer.parseInt(data[5]); - Bed.setPlayersAwake(Integer.parseInt(data[6])); - - if (Game.isMode("creative")) - Items.fillCreativeInv(Game.player.getInventory(), false); - - return true; - - case INIT: - // if (Game.debug) System.out.println("CLIENT: received INIT packet"); - if (curState != State.LOGIN) { - // System.out.println("WARNING: client received init packet in state " + - // curState + "; ignoring packet."); - return false; - } - - changeState(State.LOADING); - // curState = State.LOADING; // I don't want to do the change state sequence - // quite yet. - menu.setLoadingMessage("World"); - - String[] infostrings = alldata.split(","); - int[] info = new int[infostrings.length]; - for (int i = 0; i < info.length; i++) - info[i] = Integer.parseInt(infostrings[i]); - Game.player.eid = info[0]; - World.lvlw = info[1]; - World.lvlh = info[2]; - World.currentLevel = info[3]; - Game.player.x = info[4]; - Game.player.y = info[5]; - return true; - - case TILES: - if (curState != State.LOADING) { // ignore - if (Game.debug) - System.out.println("ignoring level tile data because client state is not LOADING: " + curState); - return false; - } - if (Game.debug) - System.out.println("CLIENT: received tiles for level " + World.currentLevel); - /// receive tiles. - Level level = World.levels[World.currentLevel]; - if (level == null) { - int lvldepth = World.idxToDepth[World.currentLevel]; - World.levels[World.currentLevel] = level = new Level(World.lvlw, World.lvlh, lvldepth, - World.levels[World.lvlIdx(lvldepth + 1)], false); - } - - String[] tilestrs = alldata.split(","); - byte[] tiledata = new byte[tilestrs.length]; - for (int i = 0; i < tiledata.length; i++) - tiledata[i] = Byte.parseByte(tilestrs[i]); - - // System.out.println("TILE DATA ARRAY AS RECEIVED BY CLIENT, DECODED BACK TO - // NUMBERS (length="+tiledata.length+"):"); - // System.out.println(Arrays.toString(tiledata)); - - if (tiledata.length / 2 > level.tiles.length) { - System.err.println( - "CLIENT ERROR: received level tile data is too long for world size; level.tiles.length=" - + level.tiles.length + ", tiles in data: " + (tiledata.length / 2) - + ". Will truncate tile loading."); - } - - for (int i = 0; i < tiledata.length / 2 && i < level.tiles.length; i++) { - level.tiles[i] = tiledata[i * 2]; - level.data[i] = tiledata[i * 2 + 1]; - } - - menu.setLoadingMessage("Entities"); - - if (World.onChangeAction != null) { - World.onChangeAction.act(); - World.onChangeAction = null; - } - - return true; - - case ENTITIES: - if (curState != State.LOADING) {// ignore - System.out.println("ignoring level entity data because client state is not LOADING: " + curState); - return false; - } - - if (Game.debug) - System.out.println("CLIENT: received entities"); - Level curLevel = World.levels[Game.currentLevel]; - Game.player.setLevel(curLevel, Game.player.x, Game.player.y); // so the shouldTrack() calls check correctly. - - String[] entities = alldata.split(","); - for (String entityString : entities) { - if (entityString.length() == 0) - continue; - - if (Game.debug) - System.out.println("CLIENT: loading entity: " + entityString); - Load.loadEntity(entityString, false); - } - - // ready to start game now. - changeState(State.PLAY); // this will be set before the client receives any cached entities, so that - // should work out. - return true; - - case TILE: - Level theLevel = World.levels[Integer.parseInt(data[0])]; - if (theLevel == null) - return false; // ignore, this is for an unvisited level. - int pos = Integer.parseInt(data[1]); - theLevel.tiles[pos] = Byte.parseByte(data[2]); - theLevel.data[pos] = Byte.parseByte(data[3]); - // if (Game.debug) System.out.println("CLIENT: updated tile on lvl " + - // theLevel.depth + " to " + Tiles.get(theLevel.tiles[pos]).name); - return true; - - case ADD: - if (curState == State.LOADING) - System.out.println("CLIENT: received entity addition while loading level"); - - // if (Game.debug) System.out.println("CLIENT: received entity addition: " + - // alldata); - - if (alldata.length() == 0) { - System.err.println("CLIENT WARNING: received entity addition is blank..."); - return false; - } - - Entity addedEntity = Load.loadEntity(alldata, false); - if (addedEntity != null) { - if (addedEntity.eid == Game.player.eid/* && Game.player.getLevel() == null */) { - if (Game.debug) - System.out.println("CLIENT: added main game player back to level based on add packet"); - World.levels[Game.currentLevel].add(Game.player); - Bed.removePlayer(Game.player); - } - - if (entityRequests.containsKey(addedEntity.eid)) - entityRequests.remove(addedEntity.eid); - } - - return true; - - case REMOVE: - if (curState == State.LOADING) - System.out.println("CLIENT: received entity removal while loading level"); - - int eid = Integer.parseInt(data[0]); - Integer entityLevelDepth; - if (data.length > 1) - entityLevelDepth = Integer.parseInt(data[1]); - else - entityLevelDepth = null; - - Entity toRemove = Network.getEntity(eid); - // if (Game.debug) System.out.println("CLIENT: received entity removal: " + - // toRemove); - if (toRemove != null) { - if (entityLevelDepth != null && toRemove.getLevel() != null - && toRemove.getLevel().depth != entityLevelDepth) { - if (Game.debug) - System.out.println("CLIENT: not removing entity " + toRemove - + " because it is not on the specified level depth, " + entityLevelDepth - + "; current depth = " + toRemove.getLevel().depth - + ". Removing from specified level only..."); - Level l = World.levels[World.lvlIdx(entityLevelDepth)]; - if (l != null) - l.remove(toRemove); - } else - toRemove.remove(); - return true; - } - return false; - - case ENTITY: - // these shouldn't occur while loading, becuase the server caches them. But just - // in case, let's make sure. - if (curState == State.LOADING) - System.out.println("CLIENT received entity update while loading level"); - - int entityid = Integer.parseInt(alldata.substring(0, alldata.indexOf(";"))); - // if (Game.debug) System.out.println("CLIENT: received entity update for: " + - // entityid); - String updates = alldata.substring(alldata.indexOf(";") + 1); - if (entityid == Game.player.eid) { - Game.player.update(updates); - return true; - } - Entity entity = Network.getEntity(entityid); - if (entity == null) { - // System.err.println("CLIENT: couldn't find entity specified to update: " + - // entityid + "; could not apply updates: " + updates); - if (entityRequests.containsKey(entityid) - && (System.nanoTime() - entityRequests.get(entityid)) / 1E8 > 15L) { // this will make it so - // that there has to be at - // least 1.5 seconds - // between each time a - // certain entity is - // requested. Also, it - // won't request the entity - // the first time around; - // it has to wait a bit - // after the first attempt - // before it will actually - // request it. - sendData(InputType.ENTITY, String.valueOf(entityid)); - entityRequests.put(entityid, System.nanoTime()); - } else if (!entityRequests.containsKey(entityid)) - entityRequests.put(entityid, (long) (System.nanoTime() - 7L * 1E8)); // should "advance" the time so - // that it only takes 0.8 - // seconds after the first - // attempt to issue the actual - // request. - return false; - } else if (!((RemotePlayer) Game.player).shouldSync(entity.x >> 4, entity.y >> 4, entity.getLevel())) { - // the entity is out of sync range; but not necessarily out of the tracking - // range, so it's *not* removed from the level here. - return false; - } else if (!((RemotePlayer) Game.player).shouldTrack(entity.x >> 4, entity.y >> 4, entity.getLevel())) { - // the entity is out of tracking range, and so may as well be removed from the - // level. - entity.remove(); - return false; - } - entity.update(updates); - return true; - - case PLAYER: - // if (Game.debug) System.out.println("CLIENT: received player packet"); - String[] playerparts = alldata.split("\\n"); - List playerinfo = Arrays.asList(playerparts[1].split(",")); - List playerinv = Arrays.asList(playerparts[2].split(",")); - Load load = new Load(new Version(playerparts[0])); - if (Game.debug) - System.out.println("CLIENT: setting player vars from packet..."); - - if (!(playerinv.size() == 1 && playerinv.get(0).equals("null"))) - load.loadInventory(Game.player.getInventory(), playerinv); - load.loadPlayer(Game.player, playerinfo); - - if (curState == State.RESPAWNING) - changeState(State.LOADING); // load the new data - return true; - - case SAVE: - if (Game.debug) - System.out.println("CLIENT: received save request"); - // send back the player data. - if (Game.debug) - System.out.println("CLIENT: sending save data"); - sendData(InputType.SAVE, Game.player.getPlayerData()); - return true; - - case NOTIFY: - if (Game.debug) - System.out.println("CLIENT: received notification"); - if (curState != State.PLAY) - return true; // ignoring for now - int notetime = Integer.parseInt(alldata.substring(0, alldata.indexOf(";"))); - String note = alldata.substring(alldata.indexOf(";") + 1); - Game.notifications.add(note); - Updater.notetick = notetime; - return true; - - case CHESTOUT: - if (curState != State.PLAY) - return false; // shouldn't happen. - Item item = Items.get(data[0]); - int idx = Integer.parseInt(data[1]); - Inventory playerInv = Game.player.getInventory(); - if (idx > playerInv.invSize()) - idx = playerInv.invSize(); - // if (Game.debug) System.out.println("CLIENT: received chestout with item: " + - // item); - if (!Game.isMode("creative")) { - Game.player.getInventory().add(idx, item); - if (Game.getMenu() instanceof ContainerDisplay) - ((ContainerDisplay) Game.getMenu()).onInvUpdate(Game.player); - } - // if (Game.debug) System.out.println("CLIENT successfully took " + item + " - // from chest and added to inv."); - return true; - - case ADDITEMS: - Inventory inv = Game.player.getInventory(); - for (String itemStr : data) - inv.add(Items.get(itemStr)); - return true; - - case INTERACT: - // the server went through with the interaction, and has sent back the new - // activeItem. - Game.player.activeItem = Items.get(alldata, true); - Game.player.resolveHeldItem(); - return true; - - case PICKUP: - if (curState != State.PLAY) - return false; // shouldn't happen. - int ieid = Integer.parseInt(alldata); - // if (Game.debug) System.out.println("CLIENT: received pickup approval for: " + - // ieid); - Entity ie = Network.getEntity(ieid); - if (ie == null || !(ie instanceof ItemEntity)) { - System.err.println( - "CLIENT error with PICKUP response: specified entity does not exist or is not an ItemEntity: " - + ieid); - return false; - } - Game.player.pickupItem((ItemEntity) ie); - return true; - - case POTION: - boolean addEffect = Boolean.parseBoolean(data[0]); - int typeIdx = Integer.parseInt(data[1]); - PotionItem.applyPotion(Game.player, PotionType.values[typeIdx], addEffect); - return true; - - case HURT: - // the player got attacked. - // if(Game.debug) System.out.println("CLIENT: received hurt packet"); - int hurteid = Integer.parseInt(data[0]); - int damage = Integer.parseInt(data[1]); - Direction attackDir = Direction.values[Integer.parseInt(data[2])]; - Entity p = Network.getEntity(hurteid); - if (p instanceof Player) - ((Player) p).hurt(damage, attackDir); - return true; - - case STAMINA: - Game.player.payStamina(Integer.parseInt(alldata)); - return true; - - case STOPFISHING: - int stopeid = Integer.parseInt(data[0]); - Entity player = Network.getEntity(stopeid); - if (player instanceof Player) { - ((Player) player).isFishing = false; - ((Player) player).fishingTicks = ((Player) player).maxFishingTicks; - } - return true; - } - - // System.out.println("CLIENT: received unexpected packet type " + inType + "; - // ignoring packet."); - return false; // this isn't reached by anything, unless it's some packet type we aren't - // looking for. So in that case, return false. - } - - /// the below methods are all about sending data to the server, *not* setting - /// any game values. - - // public void move(Player player) { move(player, player.x, player.y); } - public void move(Player player, int x, int y) { - // if(Game.debug) System.out.println("CLIENT: sending player movement to - // ("+player.x+","+player.y+"): " + player); - String movedata = x + ";" + y + ";" + player.dir.ordinal() + ";" + World.lvlIdx(player.getLevel().depth); - sendData(InputType.MOVE, movedata); - } - - /** This is called when the player.attack() method is called. */ - public void requestInteraction(Player player) { - /// I don't think the player parameter is necessary, but it doesn't harm - /// anything. - String itemString = player.activeItem != null ? player.activeItem.getData() : "null"; - sendData(InputType.INTERACT, - itemString + ";" + player.stamina + ";" + player.getInventory().count(Items.arrowItem)); - } - - public void requestTile(Level level, int xt, int yt) { - if (level == null) - return; - sendData(InputType.TILE, level.depth + ";" + xt + ";" + yt); - } - - public void dropItem(Item drop) { - sendData(InputType.DROP, drop.getData()); - } - - public void sendPlayerUpdate(Player player) { - if (player.getUpdates().length() > 0) { - sendData(InputType.PLAYER, player.getUpdates()); - player.flushUpdates(); - } - } - - public void sendPlayerDeath(Player player, DeathChest dc) { - if (player != Game.player && Game.player != null) - return; // this is client is not responsible for that player. - Level level = World.levels[Game.currentLevel]; - level.add(dc); - dc.eid = -1; - String chestData = Save.writeEntity(dc, false); - level.remove(dc); - sendData(InputType.DIE, chestData); - } - - public void requestRespawn() { - changeState(State.RESPAWNING); - } - - public void addToChest(Chest chest, int index, Item item) { - if (chest == null || item == null) - return; - sendData(InputType.CHESTIN, chest.eid + ";" + index + ";" + item.getData()); - } - - public void removeFromChest(Chest chest, int itemIndex, int inputIndex, boolean wholeStack) { - if (chest == null) - return; - sendData(InputType.CHESTOUT, chest.eid + ";" + itemIndex + ";" + wholeStack + ";" + inputIndex); - } - - public void touchDeathChest(Player player, DeathChest chest) { - sendData(InputType.CHESTOUT, chest.eid + ""); - } - - public void pushFurniture(Furniture f, Direction pushDir) { - sendData(InputType.PUSH, String.valueOf(f.eid)); - } - - public void pickupItem(ItemEntity ie) { - if (ie == null) - return; - sendData(InputType.PICKUP, String.valueOf(ie.eid)); - } - - public void sendShirtColor() { - sendData(InputType.SHIRT, Game.player.shirtColor + ""); - } - - public void sendBedRequest(Bed bed) { - sendData(InputType.BED, "true;" + String.valueOf(bed.eid)); - } - - public void sendBedExitRequest() { - sendData(InputType.BED, "false"); - } - - public void requestLevel(int lvlidx) { - if (Game.debug) - System.out.println( - "CLIENT: setting level before request to be sure, from " + Game.currentLevel + " to " + lvlidx); - Game.currentLevel = lvlidx; // just in case. - changeState(State.LOADING); - } - - public boolean checkConnection() { - // if not connected, set menu to error screen - if (!isConnected()) - menu.setError("Lost connection to server."); - return isConnected(); - } - - public void endConnection() { - if (isConnected() && curState == State.PLAY) - sendData(InputType.SAVE, Game.player.getPlayerData()); // try to make sure that the player's info is saved - // before they leave. - - super.endConnection(); - - curState = State.DISCONNECTED; - - // one may end the connection without an error; any errors should be set before - // calling this method, so there's no need to say anything here. - if (Game.debug) - System.out.println("client has ended its connection."); - } - - public boolean isConnected() { - return super.isConnected() && curState != State.DISCONNECTED; - } - - public String toString() { - return "CLIENT"; - } -} + + public static final int DEFAULT_CONNECT_TIMEOUT = 5_000; // in milliseconds + + private MultiplayerDisplay menu; + + private enum State { + LOGIN, LOADING, PLAY, RESPAWNING, DISCONNECTED + } + private State curState = State.DISCONNECTED; + + private HashMap entityRequests = new HashMap<>(); + + private int serverPlayerCount = 0; + + @Nullable + private static Socket openSocket(String hostName, MultiplayerDisplay menu, int connectTimeout) { + InetAddress hostAddress; + Socket socket; + + socket = connectWithSrv(hostName, menu, connectTimeout); + if(socket != null) + return socket; + + if (Game.debug) System.out.println("Getting host address from host name \"" + hostName + "\"..."); + + // check if there's a custom port to connect on + int port = MinicraftProtocol.PORT; + String[] splitHostName = hostName.split(":"); + if (splitHostName.length > 1) { + hostName = splitHostName[0]; + try { + port = Integer.parseInt(splitHostName[1]); + } catch (NumberFormatException exception) { + System.err.println("Invalid port: " + splitHostName[1]); + menu.setError("Invalid port"); + exception.printStackTrace(); + return null; + } + } + + try { + hostAddress = InetAddress.getByName(hostName); + } catch (UnknownHostException ex) { + System.err.println("Don't know about host " + hostName); + menu.setError("host not found"); + ex.printStackTrace(); + return null; + } + + if (Game.debug) System.out.println("Host found. Attempting to open socket..."); + + try { + socket = new Socket(); + socket.connect(new InetSocketAddress(hostAddress, port), connectTimeout); + } catch (IOException ex) { + System.err.println("Problem connecting socket to server:"); + menu.setError(ex.getMessage().replace(" (Connection refused)", "")); + ex.printStackTrace(); + return null; + } + + if (Game.debug) System.out.println("Successfully connected to game server. Returning socket..."); + + return socket; + } + + @Nullable + private static Socket connectWithSrv(String hostName, MultiplayerDisplay menu, int connectTimeout) { + // Perform an SRV lookup to determine if any exist + String query = "_minicraft._tcp." + hostName; + Record[] records; + + try { + records = new Lookup(query, Type.SRV).run(); + } catch(TextParseException e) { + if(Game.debug) System.err.println("Error running SRV lookup on '"+hostName+"', skipping."); + return null; + } + + if(records == null) { + // no records + if(Game.debug) System.out.println("No SRV records found."); + return null; + } + + // record-sorting queue + PriorityQueue queue = new PriorityQueue<>((rec1, rec2) -> { + // get smaller priority + int pComp = Integer.compare(rec1.getPriority(), rec2.getPriority()); + if(pComp != 0) + return pComp; + // get larger weight + int wComp = Integer.compare(rec2.getWeight(), rec1.getWeight()); + if(wComp != 0) + return wComp; + + // else equal + return 0; + }); + + // menu.setWaitMessage("Trying available servers"); + + // run through each record in the right order and try to connect + for(Record record: records) { + if(Game.debug) System.out.println("Found SRV record: "+record.toString()); + SRVRecord srec = (SRVRecord) record; + if(Game.debug) { + System.out.println("SRV record data:"); + System.out.println("Target: '"+srec.getTarget()+"'"); + System.out.println("Port: '"+srec.getPort()+"'"); + System.out.println("Priority: '"+srec.getPriority()+"'"); + System.out.println("Weight: '"+srec.getWeight()+"'"); + } + + queue.add(srec); + } + + // attempt to connect to each record in order of their precedence + int serverIdx = 1; + while(!queue.isEmpty()) { + SRVRecord srec = queue.poll(); + + // remove trailing . + String address = srec.getTarget().toString(); + int port = srec.getPort(); + + menu.setWaitMessage("Trying server "+serverIdx+"/"+records.length); + try { + Socket socket = new Socket(); + socket.connect(new InetSocketAddress(address, port), connectTimeout); + return socket; + } catch (IOException ex) { + System.err.println("Failed to connect to server "+serverIdx+"/"+records.length+": "+ex.getMessage()); + } + serverIdx++; + } + + menu.setWaitMessage("Trying main server"); + + return null; + } + + public MinicraftClient(String username, MultiplayerDisplay menu, String hostName) { this(username, menu, hostName, DEFAULT_CONNECT_TIMEOUT); } + public MinicraftClient(String username, MultiplayerDisplay menu, String hostName, int connectTimeout) { + super("MinicraftClient", openSocket(hostName, menu, connectTimeout)); + this.menu = menu; + Game.ISONLINE = true; + Game.ISHOST = false; + + if (super.isConnected()) { + login(username); + start(); + } + } + + public int getPlayerCount() { return serverPlayerCount; } + + private void changeState(State newState) { + if (Game.debug) System.out.println("CLIENT: Client state change from " + curState + " to " +newState); + curState = newState; + + switch(newState) { + case LOGIN: sendData(InputType.LOGIN, ((RemotePlayer)Game.player).getUsername()+ ";" +Game.VERSION); break; + + case LOADING: + Game.setMenu(menu); + menu.setLoadingMessage("Tiles"); + sendData(InputType.LOAD, String.valueOf(Game.currentLevel)); + break; + + case PLAY: + if (Game.debug) System.out.println("CLIENT: Begin game!"); + World.levels[Game.currentLevel].add(Game.player); + Renderer.readyToRenderGameplay = true; + Game.setMenu(null); + break; + + case RESPAWNING: + Game.setMenu(menu); + menu.setLoadingMessage("Spawnpoint"); + sendData(InputType.RESPAWN, ""); + break; + } + } + + private void login(String username) { + if (Game.debug) System.out.println("CLIENT: Logging in to server..."); + + try { + Game.player = new RemotePlayer(Game.player, true, InetAddress.getLocalHost(), getConnectedPort()); + ((RemotePlayer)Game.player).setUsername(username); + } catch(UnknownHostException ex) { + System.err.println("CLIENT: Could not get localhost address"); + ex.printStackTrace(); + menu.setError("unable to get localhost address"); + } + changeState(State.LOGIN); + } + + /** This method is responsible for parsing all data received by the socket. */ + public boolean parsePacket(InputType inType, String alldata) { + String[] data = alldata.split(";"); + + if (Game.packet_debug) System.out.println("Received:" + inType.toString() + ", " + alldata); + + switch(inType) { + case INVALID: + System.err.println("CLIENT: Received error: " + alldata); + menu.setError(alldata); + endConnection(); + return false; + + case PING: + sendData(InputType.PING, alldata); + return true; + + case LOGIN: + System.err.println("Server tried to login..."); + return false; + + case DISCONNECT: + if (Game.debug) System.out.println("CLIENT: Received disconnect"); + menu.setError("Server Disconnected."); // this sets the menu back to the multiplayer menu, and tells the user what happened. + endConnection(); + return true; + + case GAME: + Settings.set("mode", data[0]); + Updater.setTime(Integer.parseInt(data[1])); + Updater.gamespeed = Float.parseFloat(data[2]); + Updater.pastDay1 = Boolean.parseBoolean(data[3]); + Updater.scoreTime = Integer.parseInt(data[4]); + serverPlayerCount = Integer.parseInt(data[5]); + Bed.setPlayersAwake(Integer.parseInt(data[6])); + + if (Game.isMode("creative")) + Items.fillCreativeInv(Game.player.getInventory(), false); + + return true; + + case INIT: + + // server has validated ability to join + if (curState != State.LOGIN) { + return false; + } + + changeState(State.LOADING); + //curState = State.LOADING; // I don't want to do the change state sequence quite yet. + menu.setLoadingMessage("World"); + + String[] infostrings = alldata.split(","); + int[] info = new int[infostrings.length]; + for (int i = 0; i < info.length; i++) + info[i] = Integer.parseInt(infostrings[i]); + Game.player.eid = info[0]; + World.lvlw = info[1]; + World.lvlh = info[2]; + World.currentLevel = info[3]; + Game.player.x = info[4]; + Game.player.y = info[5]; + return true; + + case TILES: + if (curState != State.LOADING) { // ignore + if (Game.debug) System.out.println("Ignoring level tile data because client state is not LOADING: " + curState); + return false; + } + if (Game.debug) System.out.println("CLIENT: Received tiles for level " + World.currentLevel); + + Level level = World.levels[World.currentLevel]; // receive tiles. + if (level == null) { + int lvldepth = World.idxToDepth[World.currentLevel]; + World.levels[World.currentLevel] = level = new Level(World.lvlw, World.lvlh, lvldepth, World.levels[World.lvlIdx(lvldepth+1)], false); + } + + String[] tilestrs = alldata.split(","); + byte[] tiledata = new byte[tilestrs.length]; + for (int i = 0; i < tiledata.length; i++) + tiledata[i] = Byte.parseByte(tilestrs[i]); + + if (tiledata.length / 2 > level.tiles.length) { + System.err.println("CLIENT ERROR: Received level tile data is too long for world size; level.tiles.length=" + level.tiles.length + ", tiles in data: " + (tiledata.length / 2) + ". Will truncate tile loading."); + } + + for (int i = 0; i < tiledata.length/2 && i < level.tiles.length; i++) { + level.tiles[i] = tiledata[i*2]; + level.data[i] = tiledata[i*2+1]; + } + + menu.setLoadingMessage("Entities"); + + if (World.onChangeAction != null) { + World.onChangeAction.act(); + World.onChangeAction = null; + } + + return true; + + case ENTITIES: + if (curState != State.LOADING) {// ignore + System.out.println("Ignoring level entity data because client state is not LOADING: " + curState); + return false; + } + + if (Game.debug) System.out.println("CLIENT: Received entities"); + Level curLevel = World.levels[Game.currentLevel]; + Game.player.setLevel(curLevel, Game.player.x, Game.player.y); // so the shouldTrack() calls check correctly. + + String[] entities = alldata.split(","); + for (String entityString: entities) { + if (entityString.length() == 0) continue; + + if (Game.debug) System.out.println("CLIENT: Loading entity: " + entityString); + Load.loadEntity(entityString, false); + } + + // Ready to start game now. + changeState(State.PLAY); // This will be set before the client receives any cached entities, so that should work out. + return true; + + case TILE: + Level theLevel = World.levels[Integer.parseInt(data[0])]; + if (theLevel == null) return false; // ignore, this is for an unvisited level. + int pos = Integer.parseInt(data[1]); + theLevel.tiles[pos] = Byte.parseByte(data[2]); + theLevel.data[pos] = Byte.parseByte(data[3]); + return true; + + case ADD: + if (curState == State.LOADING) + System.out.println("CLIENT: Received entity addition while loading level"); + + if (alldata.length() == 0) { + System.err.println("CLIENT WARNING: Received entity addition is blank..."); + return false; + } + + Entity addedEntity = Load.loadEntity(alldata, false); + if (addedEntity != null) { + if (addedEntity.eid == Game.player.eid) { + if (Game.debug) System.out.println("CLIENT: Added main game player back to level based on add packet"); + World.levels[Game.currentLevel].add(Game.player); + Bed.removePlayer(Game.player); + } + + entityRequests.remove(addedEntity.eid); + } + + return true; + + case REMOVE: + if (curState == State.LOADING) + System.out.println("CLIENT: Received entity removal while loading level"); + + int eid = Integer.parseInt(data[0]); + Integer entityLevelDepth; + if (data.length > 1) + entityLevelDepth = Integer.parseInt(data[1]); + else + entityLevelDepth = null; + + Entity toRemove = Network.getEntity(eid); + if (toRemove != null) { + if (entityLevelDepth != null && toRemove.getLevel() != null && toRemove.getLevel().depth != entityLevelDepth) { + if (Game.debug) System.out.println("CLIENT: Not removing entity " + toRemove + " because it is not on the specified level depth, " + entityLevelDepth + "; current depth = " + toRemove.getLevel().depth + ". Removing from specified level only..."); + Level l = World.levels[World.lvlIdx(entityLevelDepth)]; + if (l != null) + l.remove(toRemove); + } + else + toRemove.remove(); + return true; + } + return false; + + case ENTITY: // TODO: Make this method easier to look at + // these shouldn't occur while loading, becuase the server caches them. But just in case, let's make sure. + if (curState == State.LOADING) + System.out.println("CLIENT: Received entity update while loading level"); + + int entityid = Integer.parseInt(alldata.substring(0, alldata.indexOf(";"))); + String updates = alldata.substring(alldata.indexOf(";")+1); + if (entityid == Game.player.eid) { + Game.player.update(updates); + return true; + } + Entity entity = Network.getEntity(entityid); + if (entity == null) { + if (entityRequests.containsKey(entityid) && (System.nanoTime() - entityRequests.get(entityid))/1E8 > 15L) { // this will make it so that there has to be at least 1.5 seconds between each time a certain entity is requested. Also, it won't request the entity the first time around; it has to wait a bit after the first attempt before it will actually request it. + sendData(InputType.ENTITY, String.valueOf(entityid)); + entityRequests.put(entityid, System.nanoTime()); + } + else if (!entityRequests.containsKey(entityid)) + entityRequests.put(entityid, (long)(System.nanoTime() - 7L*1E8)); // should "advance" the time so that it only takes 0.8 seconds after the first attempt to issue the actual request. + return false; + } + else if (!((RemotePlayer)Game.player).shouldSync(entity.x >> 4, entity.y >> 4, entity.getLevel())) { // the entity is out of sync range; but not necessarily out of the tracking range, so it's *not* removed from the level here. + return false; + } + else if (!((RemotePlayer)Game.player).shouldTrack(entity.x >> 4, entity.y >> 4, entity.getLevel())) { // the entity is out of tracking range, and so may as well be removed from the level. + entity.remove(); + return false; + } + entity.update(updates); + return true; + + case PLAYER: + String[] playerparts = alldata.split("\\n"); + List playerinfo = Arrays.asList(playerparts[1].split(",")); + List playerinv = Arrays.asList(playerparts[2].split(",")); + Load load = new Load(new Version(playerparts[0])); + if (Game.debug) System.out.println("CLIENT: Setting player vars from packet..."); + + if (!(playerinv.size() == 1 && playerinv.get(0).equals("null"))) + load.loadInventory(Game.player.getInventory(), playerinv); + load.loadPlayer(Game.player, playerinfo); + + if (curState == State.RESPAWNING) + changeState(State.LOADING); // load the new data + return true; + + case SAVE: + if (Game.debug) System.out.println("CLIENT: Received save request"); + if (Game.debug) System.out.println("CLIENT: Sending save data"); + sendData(InputType.SAVE, Game.player.getPlayerData()); // send back the player data. + return true; + + case NOTIFY: + if (Game.debug) System.out.println("CLIENT: Received notification"); + if (curState != State.PLAY) return true; // ignoring for now + int notetime = Integer.parseInt(alldata.substring(0, alldata.indexOf(";"))); + String note = alldata.substring(alldata.indexOf(";")+1); + Game.notifications.add(note); + Updater.notetick = notetime; + return true; + + case CHESTOUT: + if (curState != State.PLAY) return false; // shouldn't happen. + Item item = Items.get(data[0]); + int idx = Integer.parseInt(data[1]); + Inventory playerInv = Game.player.getInventory(); + if (idx > playerInv.invSize()) + idx = playerInv.invSize(); + if (!Game.isMode("creative")) { + Game.player.getInventory().add(idx, item); + if (Game.getMenu() instanceof ContainerDisplay) + ((ContainerDisplay)Game.getMenu()).onInvUpdate(Game.player); + } + return true; + + case ADDITEMS: + Inventory inv = Game.player.getInventory(); + for (String itemStr: data) + inv.add(Items.get(itemStr)); + return true; + + case INTERACT: + // the server went through with the interaction, and has sent back the new activeItem. + Game.player.activeItem = Items.get(alldata, true); + Game.player.resolveHeldItem(); + return true; + + case PICKUP: + if (curState != State.PLAY) return false; // shouldn't happen. + int ieid = Integer.parseInt(alldata); + Entity ie = Network.getEntity(ieid); + if (ie == null || !(ie instanceof ItemEntity)) { + System.err.println("CLIENT: Error with PICKUP response: specified entity does not exist or is not an ItemEntity: " + ieid); + return false; + } + Game.player.pickupItem((ItemEntity)ie); + return true; + + case POTION: + boolean addEffect = Boolean.parseBoolean(data[0]); + int typeIdx = Integer.parseInt(data[1]); + PotionItem.applyPotion(Game.player, PotionType.values[typeIdx], addEffect); + return true; + + case HURT: + // the player got attacked. + int hurteid = Integer.parseInt(data[0]); + int damage = Integer.parseInt(data[1]); + Direction attackDir = Direction.values[Integer.parseInt(data[2])]; + Entity p = Network.getEntity(hurteid); + if (p instanceof Player) + ((Player)p).hurt(damage, attackDir); + return true; + + case STAMINA: + Game.player.payStamina(Integer.parseInt(alldata)); + return true; + + case STOPFISHING: + int stopeid = Integer.parseInt(data[0]); + Entity player = Network.getEntity(stopeid); + if (player instanceof Player) { + ((Player) player).isFishing = false; + ((Player) player).fishingTicks = ((Player) player).maxFishingTicks; + } + return true; + } + return false; // this isn't reached by anything, unless it's some packet type we aren't looking for. So in that case, return false. + } + + // The below methods are all about sending data to the server, *not* setting any game values. + + public void move(Player player, int x, int y) { + String movedata = x+ ";" +y+ ";" +player.dir.ordinal()+ ";" +World.lvlIdx(player.getLevel().depth); + sendData(InputType.MOVE, movedata); + } + + /** This is called when the player.attack() method is called. */ + public void requestInteraction(Player player) { + // I don't think the player parameter is necessary, but it doesn't harm anything. + String itemString = player.activeItem != null ? player.activeItem.getData() : "null"; + sendData(InputType.INTERACT, itemString+ ";" +player.stamina+ ";" +player.getInventory().count(Items.arrowItem)); + } + + public void requestTile(Level level, int xt, int yt) { + if (level == null) return; + sendData(InputType.TILE, level.depth+ ";" +xt+ ";" +yt); + } + + public void dropItem(Item drop) { sendData(InputType.DROP, drop.getData()); } + + public void sendPlayerUpdate(Player player) { + if (player.getUpdates().length() > 0) { + sendData(InputType.PLAYER, player.getUpdates()); + player.flushUpdates(); + } + } + + public void sendPlayerDeath(Player player, DeathChest dc) { + if (player != Game.player && Game.player != null) return; // this is client is not responsible for that player. + Level level = World.levels[Game.currentLevel]; + level.add(dc); + dc.eid = -1; + String chestData = Save.writeEntity(dc, false); + level.remove(dc); + sendData(InputType.DIE, chestData); + } + + public void requestRespawn() { changeState(State.RESPAWNING); } + + public void addToChest(Chest chest, int index, Item item) { + if (chest == null || item == null) return; + sendData(InputType.CHESTIN, chest.eid+ ";" +index+ ";" +item.getData()); + } + + public void removeFromChest(Chest chest, int itemIndex, int inputIndex, boolean wholeStack) { + if (chest == null) return; + sendData(InputType.CHESTOUT, chest.eid+ ";" +itemIndex+ ";" +wholeStack+ ";" +inputIndex); + } + + public void touchDeathChest(DeathChest chest) { + sendData(InputType.CHESTOUT, chest.eid+ ""); + } + + public void pushFurniture(Furniture f) { sendData(InputType.PUSH, String.valueOf(f.eid)); } + + public void pickupItem(ItemEntity ie) { + if (ie == null) return; + sendData(InputType.PICKUP, String.valueOf(ie.eid)); + } + + public void sendShirtColor() { sendData(InputType.SHIRT, Game.player.shirtColor+ ""); } + + public void sendBedRequest(Bed bed) { sendData(InputType.BED, "true;" + bed.eid); } + public void sendBedExitRequest() { sendData(InputType.BED, "false"); } + + public void requestLevel(int lvlidx) { + if (Game.debug) System.out.println("CLIENT: Setting level before request to be sure, from " +Game.currentLevel+ " to " +lvlidx); + Game.currentLevel = lvlidx; // just in case. + changeState(State.LOADING); + } + + public boolean checkConnection() { + // if not connected, set menu to error screen + if (!isConnected()) + menu.setError("Lost connection to server."); + return isConnected(); + } + + public void endConnection() { + if (isConnected() && curState == State.PLAY) + sendData(InputType.SAVE, Game.player.getPlayerData()); // try to make sure that the player's info is saved before they leave. + + super.endConnection(); + + curState = State.DISCONNECTED; + + // one may end the connection without an error; any errors should be set before calling this method, so there's no need to say anything here. + if (Game.debug) System.out.println("Client has ended its connection."); + } + + public boolean isConnected() { return super.isConnected() && curState != State.DISCONNECTED; } + + public String toString() { return "CLIENT"; } +} \ No newline at end of file diff --git a/src/minicraft/network/MinicraftConnection.java b/src/minicraft/network/MinicraftConnection.java index 33c67364..1f810ce1 100644 --- a/src/minicraft/network/MinicraftConnection.java +++ b/src/minicraft/network/MinicraftConnection.java @@ -14,149 +14,123 @@ import minicraft.item.PotionType; public abstract class MinicraftConnection extends Thread implements MinicraftProtocol { - - private PrintWriter out; - private BufferedReader in; - private Socket socket; - - protected MinicraftConnection(String threadName, @Nullable Socket socket) { - super(threadName); - this.socket = socket; - - if (socket == null) - return; - - try { - in = new BufferedReader(new InputStreamReader(socket.getInputStream())); - out = new PrintWriter(socket.getOutputStream(), true); - } catch (IOException ex) { - System.err.println("failed to initialize i/o streams for socket:"); - ex.printStackTrace(); - } catch (NullPointerException ex) { - System.err.println("CONNECTION ERROR: null socket, cannot initialize i/o streams..."); - ex.printStackTrace(); - } - } - - public void run() { - if (Game.debug) - System.out.println("starting " + this); - - StringBuilder currentData = new StringBuilder(); - - while (isConnected()) { - int read = -2; - - try { - read = in.read(); - } catch (IOException ex) { - System.err.println( - this + " had a problem reading its input stream (will continue trying): " + ex.getMessage()); - ex.printStackTrace(); - } - - if (read < 0) { - if (Game.debug) - System.out.println(this + " reached end of input stream."); - break; - } - - // if (Game.debug) System.out.println(this + " successfully read character from - // input stream: " + read); - - if (read > 0) { // if it is valid character that is not the null character, then add it to the - // string. - currentData.append((char) read); - } else if (currentData.length() > 0) { // read MUST equal 0 at this point, aka a null character; the if - // statement makes it ignore sequential null characters. - - // if (Game.debug) System.out.println(this + " completed data packet: " + - // currentData); - - InputType inType = MinicraftProtocol.getInputType(currentData.charAt(0)); - - if (inType == null) - System.err.println("SERVER: invalid packet received; input type is not valid."); - else - parsePacket(inType, currentData.substring(1)); - - currentData = new StringBuilder(); - // if (Game.debug) System.out.println(this + " cleared currentData."); - } - } - - if (Game.debug) - System.out.println("run loop ended for " + this + "; ending connection."); - - endConnection(); - } - - protected int getConnectedPort() { - return socket.getPort(); - } - - protected abstract boolean parsePacket(InputType inType, String data); - - protected void sendData(InputType inType, String data) { - if (socket == null) - return; - - if (Game.packet_debug && Game.isConnectedClient()) - System.out.println("Sent:" + inType.toString() + ", " + data); - - char inTypeChar = (char) (inType.ordinal() + 1); - // if (Game.debug && inType == InputType.TILES) System.out.println(this + ": - // printing " + inType + " data:"); - if (data.contains("\0")) - System.err.println("WARNING from " + this + ": data to send contains a null character. Not sending data."); - else { - out.print(inTypeChar + data + '\0'); - out.flush(); - } - } - - @NotNull - public static String stringToInts(String str) { - return stringToInts(str, str.length()); - } - - @NotNull - public static String stringToInts(String str, int maxLength) { - int[] chars = new int[Math.min(str.length(), maxLength)]; - - for (int i = 0; i < chars.length; i++) - chars[i] = (int) str.charAt(i); - - return Arrays.toString(chars); - } - - /// there are a couple methods that are identical in both a server thread, and - /// the client, so I'll just put them here. - - public void sendNotification(String note, int notetime) { - sendData(InputType.NOTIFY, notetime + ";" + note); - } - - public void sendPotionEffect(PotionType type, boolean addEffect) { - sendData(InputType.POTION, addEffect + ";" + type.ordinal()); - } - - public void endConnection() { - if (socket != null && !socket.isClosed()) { - if (Game.debug) - System.out.println("closing socket and ending connection for: " + this); - - sendData(InputType.DISCONNECT, ""); - - try { - socket.close(); - } catch (IOException ignored) { - } - } - } - - public boolean isConnected() { - - return socket != null && !socket.isClosed() && socket.isConnected(); - } -} + + private PrintWriter out; + private BufferedReader in; + private Socket socket; + + protected MinicraftConnection(String threadName, @Nullable Socket socket) { + super(threadName); + this.socket = socket; + + if (socket == null) return; + + try { + in = new BufferedReader(new InputStreamReader(socket.getInputStream())); + out = new PrintWriter(socket.getOutputStream(), true); + } catch (IOException ex) { + System.err.println("Failed to initialize i/o streams for socket:"); + ex.printStackTrace(); + } catch (NullPointerException ex) { + System.err.println("CONNECTION ERROR: null socket, cannot initialize i/o streams..."); + ex.printStackTrace(); + } + } + + public void run() { + if (Game.debug) System.out.println("Starting " + this); + + StringBuilder currentData = new StringBuilder(); + + while(isConnected()) { + int read = -2; + + try { + read = in.read(); + } catch (IOException ex) { + System.err.println(this + " had a problem reading its input stream (will continue trying): " + ex.getMessage()); + ex.printStackTrace(); + } + + if (read < 0) { + if (Game.debug) System.out.println(this + " reached end of input stream."); + break; + } + + if (read > 0) { // if it is valid character that is not the null character, then add it to the string. + currentData.append( (char)read ); + } + else if (currentData.length() > 0) { // read MUST equal 0 at this point, aka a null character; the if statement makes it ignore sequential null characters. + + InputType inType = MinicraftProtocol.getInputType(currentData.charAt(0)); + + if (inType == null) + System.err.println("SERVER: invalid packet received; input type is not valid."); + else + parsePacket(inType, currentData.substring(1)); + + currentData = new StringBuilder(); + } + } + + if (Game.debug) System.out.println("Run loop ended for " + this + "; ending connection."); + + endConnection(); + } + + protected int getConnectedPort() { + return socket.getPort(); + } + + protected abstract boolean parsePacket(InputType inType, String data); + + protected void sendData(InputType inType, String data) { + if (socket == null) return; + + if (Game.packet_debug && Game.isConnectedClient()) System.out.println("Sent:" + inType.toString() + ", " + data); + + char inTypeChar = (char) (inType.ordinal() + 1); + if (data.contains("\0")) System.err.println("WARNING from " + this + ": data to send contains a null character. Not sending data."); + else { + out.print(inTypeChar + data + '\0'); + out.flush(); + } + } + + @NotNull + public static String stringToInts(String str, int maxLength) { + int[] chars = new int[Math.min(str.length(), maxLength)]; + + for (int i = 0; i < chars.length; i++) + chars[i] = (int) str.charAt(i); + + return Arrays.toString(chars); + } + + // there are a couple methods that are identical in both a server thread, and the client, so I'll just put them here. + + public void sendNotification(String note, int notetime) { + sendData(InputType.NOTIFY, notetime+ ";" + note); + } + + public void sendPotionEffect(PotionType type, boolean addEffect) { + sendData(InputType.POTION, addEffect+ ";" + type.ordinal()); + } + + public void endConnection() { + if (socket != null && !socket.isClosed()) { + if (Game.debug) System.out.println("Closing socket and ending connection for: " + this); + + sendData(InputType.DISCONNECT, ""); + + try { + socket.close(); + } catch (IOException ignored) { + } + } + } + + public boolean isConnected() { + return socket != null && !socket.isClosed() && socket.isConnected(); + } +} \ No newline at end of file diff --git a/src/minicraft/network/MinicraftProtocol.java b/src/minicraft/network/MinicraftProtocol.java index 59795538..187a3f07 100644 --- a/src/minicraft/network/MinicraftProtocol.java +++ b/src/minicraft/network/MinicraftProtocol.java @@ -5,38 +5,33 @@ import java.util.List; public interface MinicraftProtocol { - - int PORT = 4225; - - enum InputType { - INVALID, PING, USERNAMES, LOGIN, GAME, INIT, LOAD, TILES, ENTITIES, TILE, ENTITY, PLAYER, MOVE, ADD, REMOVE, - DISCONNECT, SAVE, NOTIFY, INTERACT, PUSH, PICKUP, CHESTIN, CHESTOUT, ADDITEMS, BED, POTION, HURT, DIE, RESPAWN, - DROP, STAMINA, SHIRT, STOPFISHING; - - public static final InputType[] values = InputType.values(); - public static final List serverOnly = Arrays.asList(INIT, TILES, ENTITIES, ADD, REMOVE, HURT, GAME, - ADDITEMS, STAMINA, STOPFISHING); - public static final List entityUpdates = Arrays.asList(ENTITY, ADD, REMOVE); - public static final List tileUpdates = Collections.singletonList(TILE); - } - - static InputType getInputType(char idxChar) { - InputType inType; - int idx = idxChar; - idx--; // the "-1" is because 1 is added originally so it does not make a null - // character, which is used to seperate requests. - - if (idx < InputType.values.length && idx >= 0) - inType = InputType.values[idx]; - else { - System.err.println("Communication Error: Socket data has an invalid input type: " + idx); - return null; - } - - return inType; - } - - void endConnection(); - - boolean isConnected(); -} + + int PORT = 4225; + + enum InputType { + INVALID, PING, USERNAMES, LOGIN, GAME, INIT, LOAD, TILES, ENTITIES, TILE, ENTITY, PLAYER, MOVE, ADD, REMOVE, DISCONNECT, SAVE, NOTIFY, INTERACT, PUSH, PICKUP, CHESTIN, CHESTOUT, ADDITEMS, BED, POTION, HURT, DIE, RESPAWN, DROP, STAMINA, SHIRT, STOPFISHING; + + public static final InputType[] values = InputType.values(); + public static final List serverOnly = Arrays.asList(INIT, TILES, ENTITIES, ADD, REMOVE, HURT, GAME, ADDITEMS, STAMINA, STOPFISHING); + public static final List entityUpdates = Arrays.asList(ENTITY, ADD, REMOVE); + public static final List tileUpdates = Collections.singletonList(TILE); + } + + static InputType getInputType(char idxChar) { + InputType inType; + int idx = idxChar; + idx--; // the "-1" is because 1 is added originally so it does not make a null character, which is used to seperate requests. + + if(idx < InputType.values.length && idx >= 0) + inType = InputType.values[idx]; + else { + System.err.println("Communication Error: Socket data has an invalid input type: " + idx); + return null; + } + + return inType; + } + + void endConnection(); + boolean isConnected(); +} \ No newline at end of file diff --git a/src/minicraft/network/MinicraftServer.java b/src/minicraft/network/MinicraftServer.java index e4692c64..dac26ecb 100644 --- a/src/minicraft/network/MinicraftServer.java +++ b/src/minicraft/network/MinicraftServer.java @@ -43,896 +43,785 @@ import minicraft.screen.WorldSelectDisplay; public class MinicraftServer extends Thread implements MinicraftProtocol { - - class MyTask extends TimerTask { - public MyTask() { - } - - public void run() { - } - } - - private static final int UPDATE_INTERVAL = 10; // measured in seconds - - private final int port; - - private List threadList = Collections.synchronizedList(new ArrayList<>()); - private ServerSocket socket; - - private RemotePlayer hostPlayer = null; - private String worldPath; - - private int playerCap = 10; - - public MinicraftServer(int port) { - super("MinicraftServer"); - - this.port = port; - - Game.ISONLINE = true; - Game.ISHOST = true; // just in case. - Game.player.remove(); // the server has no player... - - worldPath = Game.gameDir + "/saves/" + WorldSelectDisplay.getWorldName(); - - try { - System.out.println("Opening server socket..."); - socket = new ServerSocket(port); - start(); - } catch (IOException ex) { - System.err.println("Failed to open server socket on port " + port); - ex.printStackTrace(); - } - - } - - public void run() { - if (Game.debug) - System.out.println("Server started."); - - Timer gameUpdateTimer = new Timer("GameUpdateTimer"); - gameUpdateTimer.schedule((new MyTask() { - public void run() { - updateGameVars(); - } - }), 5000, UPDATE_INTERVAL * 1000); - - try { - while (socket != null) { - MinicraftServerThread mst = new MinicraftServerThread(socket.accept(), this); - if (mst.isConnected()) - threadList.add(mst); - } - } catch (SocketException ex) { // this should occur when closing the thread. - } catch (IOException ex) { - System.err.println("Server socket encountered an error while attempting to listen on port " + port + ":"); - ex.printStackTrace(); - } - - gameUpdateTimer.cancel(); - System.out.println("Closing server socket"); - - endConnection(); - } - - public String getWorldPath() { - return worldPath; - } - - public int getPlayerCap() { - return playerCap; - } - - public void setPlayerCap(int val) { - playerCap = Math.max(val, -1); // no need to set it to anything below -1. - } - - public boolean isFull() { - return playerCap >= 0 && threadList.size() >= playerCap; - } - - public int getNumPlayers() { - return threadList.size(); - } - - public MinicraftServerThread[] getThreads() { - return threadList.toArray(new MinicraftServerThread[threadList.size()]); - } - - public String[] getClientInfo() { - List playerStrings = new ArrayList<>(); - for (MinicraftServerThread serverThread : getThreads()) { - RemotePlayer clientPlayer = serverThread.getClient(); - - playerStrings.add(clientPlayer.getUsername() + ": " + clientPlayer.getIpAddress().getHostAddress() - + (Game.debug ? " (" + (clientPlayer.x >> 4) + "," + (clientPlayer.y >> 4) + ")" : "")); - } - - return playerStrings.toArray(new String[0]); - } - - public List getPlayersInRange(Entity e, boolean useTrackRange) { - if (e == null || e.getLevel() == null) - return new ArrayList<>(); - int xt = e.x >> 4, yt = e.y >> 4; - return getPlayersInRange(e.getLevel(), xt, yt, useTrackRange); // NOTE if "e" is a RemotePlayer, the list - // returned *will* contain "e". - } - - public List getPlayersInRange(Level level, int xt, int yt, boolean useTrackRange) { - List players = new ArrayList<>(); - for (MinicraftServerThread thread : getThreads()) { - RemotePlayer rp = thread.getClient(); - if (useTrackRange && rp.shouldTrack(xt, yt, level) || !useTrackRange && rp.shouldSync(xt, yt, level)) - players.add(rp); - } - - return players; - } - - @Nullable - private RemotePlayer getIfPlayer(Entity e) { - if (e instanceof RemotePlayer) { - return (RemotePlayer) e; - } else - return null; - } - - @Nullable - public MinicraftServerThread getAssociatedThread(String username) { - MinicraftServerThread match = null; - - for (MinicraftServerThread thread : getThreads()) { - if (thread.getClient().getUsername().equalsIgnoreCase(username)) { - match = thread; - break; - } - } - - return match; - } - - public List getAssociatedThreads(String[] usernames, boolean printError) { - List threads = new ArrayList<>(); - for (String username : usernames) { - MinicraftServerThread match = getAssociatedThread(username); - if (match != null) - threads.add(match); - else if (printError) - System.err.println("Couldn't match username \"" + username + "\""); - } - - return threads; - } - - @NotNull - public MinicraftServerThread getAssociatedThread(RemotePlayer player) { - MinicraftServerThread thread = null; - - for (MinicraftServerThread curThread : getThreads()) { - if (curThread.getClient() == player) { - thread = curThread; - break; - } - } - - if (thread == null) { - System.err.println("SERVER: Could not find thread for remote player " + player); - thread = new MinicraftServerThread(player, this); - } - - return thread; - } - - private List getAssociatedThreads(List players) { - List threads = new ArrayList<>(); - - // NOTE I could do this the other way around, by looping though the thread list, - // and adding those whose player is found in the given list, which might be - // slightly more optimal... but I think it's better that this tells you when a - // player in the list doesn't have a matching thread. - for (RemotePlayer client : players) { - MinicraftServerThread thread = getAssociatedThread(client); - if (thread.isValid()) - threads.add(thread); - else - System.err.println("SERVER WARNING: Couldn't find server thread for client " + client); - } - - return threads; - } - - public void broadcastEntityUpdate(Entity e) { - broadcastEntityUpdate(e, false); - } - - public void broadcastEntityUpdate(Entity e, boolean updateSelf) { - if (e.isRemoved()) { - if (Game.debug) - System.out.println("SERVER: Tried to broadcast update of removed entity: " + e); - return; - } - List players = getPlayersInRange(e, false); - if (!updateSelf) { - players.remove(getIfPlayer(e)); - } - - for (MinicraftServerThread thread : getAssociatedThreads(players)) { - thread.sendEntityUpdate(e, e.getUpdates()); - } - - e.flushUpdates(); // It is important that this method is only called once: right here. - } - - public void broadcastTileUpdate(Level level, int x, int y) { - broadcastData(InputType.TILE, Tile.getData(level.depth, x, y)); - } - - public void broadcastEntityAddition(Entity e) { - broadcastEntityAddition(e, false); - } - - public void broadcastEntityAddition(Entity e, boolean addSelf) { - if (e.isRemoved()) { - if (Game.debug) - System.out.println("SERVER: Tried to broadcast addition of removed entity: " + e); - return; - } - List players = getPlayersInRange(e, true); - if (!addSelf) - players.remove(getIfPlayer(e)); // if "e" is a player, this removes it from the list. - int cnt = 0; - if (Game.debug && e instanceof Player) - System.out.println("SERVER: Broadcasting player addition of " + e); - for (MinicraftServerThread thread : getAssociatedThreads(players)) { - thread.sendEntityAddition(e); - cnt++; - } - if (Game.debug && e instanceof Player) - System.out.println("SERVER: Broadcasted player addition of " + e + " to " + cnt + " clients"); - } - - private List getPlayersToRemove(Entity e, boolean removeSelf) { - List players = getPlayersInRange(e, true); - if (players.size() == 0) - return players; - if (Game.debug && e instanceof Player) { - System.out.println("SERVER: Sending removal of player " + e + " to " + players.size() - + " players (may remove equal player): "); - for (RemotePlayer rp : players) - System.out.println(rp); - } - - if (!removeSelf) - players.remove(getIfPlayer(e)); // if "e" is a player, this removes it from the list. - - if (Game.debug && e instanceof Player) - System.out.println("...now sending player removal to " + players.size() + " players."); - - return players; - } - - // remove only if on given level - public void broadcastEntityRemoval(Entity e, Level level, boolean removeSelf) { - List players = getPlayersToRemove(e, removeSelf); - - if (level == null) { - if (Game.debug) - System.out.println("SERVER: Cannot remove entity " + e - + " from specified level, level given is null; ignoring request to broadcast entity removal."); - return; - } - - for (MinicraftServerThread thread : getAssociatedThreads(players)) - thread.sendEntityRemoval(e.eid, level.depth); - } - - // remove regardless of level - public void broadcastEntityRemoval(Entity e, boolean removeSelf) { - List players = getPlayersToRemove(e, removeSelf); - - for (MinicraftServerThread thread : getAssociatedThreads(players)) - thread.sendEntityRemoval(e.eid); - } - - public void saveWorld() { - broadcastData(InputType.SAVE, ""); // tell all the other clients to send their data over to be saved. - new Save(WorldSelectDisplay.getWorldName()); - System.out.println("World saved"); - } - - public void broadcastNotification(String note, int notetime) { - String data = notetime + ";" + note; - broadcastData(InputType.NOTIFY, data); - } - - public void broadcastPlayerHurt(int eid, int damage, Direction attackDir) { - for (MinicraftServerThread thread : getThreads()) - thread.sendPlayerHurt(eid, damage, attackDir); - } - - public void broadcastStopFishing(int eid) { - for (MinicraftServerThread thread : getThreads()) - thread.sendStopFishing(eid); - } - - public void updateGameVars() { - updateGameVars(getThreads()); - } - - public void updateGameVars(MinicraftServerThread sendTo) { - updateGameVars(new MinicraftServerThread[] { sendTo }); - } - - public void updateGameVars(MinicraftServerThread[] sendTo) { - // if (Game.debug) System.out.println("SERVER: updating game vars..."); - if (sendTo.length == 0) - return; - - String[] varArray = { Settings.get("mode").toString(), Updater.tickCount + "", Updater.gamespeed + "", - Updater.pastDay1 + "", Updater.scoreTime + "", getNumPlayers() + "", Bed.getPlayersAwake() + "" }; - - String vars = String.join(";", varArray); - - for (MinicraftServerThread thread : sendTo) - thread.sendData(InputType.GAME, vars); - } - - public void pingClients() { - System.out.println("Pinging clients (" + threadList.size() + " connected)..."); - for (MinicraftServerThread thread : getThreads()) - thread.doPing(); - } - - protected File[] getRemotePlayerFiles() { - File saveFolder = new File(worldPath); - - File[] clientSaves = saveFolder.listFiles((file, name) -> name.startsWith("RemotePlayer")); - - if (clientSaves == null) - clientSaves = new File[0]; - - return clientSaves; - } - - boolean parsePacket(MinicraftServerThread serverThread, InputType inType, String alldata) { - String[] data = alldata.split(";"); - - RemotePlayer clientPlayer = serverThread.getClient(); - if (clientPlayer == null) { - System.err.println("CRITICAL SERVER ERROR: Server thread client is null: " + serverThread - + "; cannot parse the received " + inType + " packet: " + alldata); - return false; - } - - // handle reports of type INVALID - if (inType == InputType.INVALID) { - if (Game.debug) - System.out.println(serverThread + " received an error:"); - System.err.println(alldata); - return false; - } - - if (InputType.serverOnly.contains(inType)) { - /// these are ALL illegal for a client to send. - System.err.println( - "SERVER WARNING: Client " + clientPlayer + " sent illegal packet type " + inType + " to server."); - return false; - } - - switch (inType) { - case PING: - System.out.println("Received ping from " + serverThread); - return true; - - case LOGIN: - if (Game.debug) - System.out.println("SERVER: Received login request"); - if (Game.debug) - System.out.println("SERVER: Login data: " + Arrays.toString(data)); - String username = data[0]; - Version clientVersion = new Version(data[1]); - // check version; send back invalid if they don't match. - if (clientVersion.compareTo(Game.VERSION) != 0) { - serverThread.sendError("Wrong game version; need " + Game.VERSION); - return false; - } - - // check if the same username already exists on the server (due to signing on a - // second time with the same account), and if so, prevent the new login - if (getAssociatedThread(username) != null) { - serverThread.sendError("Account is already logged in to server"); - return false; - } - - // versions match, and username is unique; make client player - clientPlayer.setUsername(username); - - // now, we need to check if this player has played in this world before. If they - // have, then all previous settings and items and such will be restored. - String playerdata = ""; // this stores the data fetched from the files. - - if (serverThread.getClient().getIpAddress().isLoopbackAddress() && hostPlayer == null) { - // this is the first person on localhost. I believe that a second person will be - // saved as a loopback address (later: jk the first one actually will), but a - // third will simply overwrite the second. - if (Game.debug) - System.out.println("SERVER: Host player found"); - - if (Game.player != null) { - // save the player, and then remove it. It is leftover from when this was a - // single player world. - playerdata = Game.player.getPlayerData(); - Game.player.remove(); // all the important data has been saved. - Game.player = null; - } else { - // load the data from file instead. - playerdata = Game.VERSION + "\n"; - try { - playerdata += Load.loadFromFile(worldPath + "/Player" + Save.extension, true) + "\n"; - playerdata += Load.loadFromFile(worldPath + "/Inventory" + Save.extension, true); - } catch (IOException ex) { - System.err.println("SERVER: Server had error while trying to load host player data from file:"); - ex.printStackTrace(); - } - } - - hostPlayer = clientPlayer; - } else { - playerdata = serverThread.getRemotePlayerFileData(); - } - - if (playerdata.length() > 0) { - // if a save file was found, then send the data to the client so they can resume - // where they left off. - // and now, initialize the RemotePlayer instance with the data. - // first get the version that saved the file, which can be different if this is - // a remote player. - String[] saveData = playerdata.split("\\n"); - new Load(new Version(saveData[0])).loadPlayer(clientPlayer, Arrays.asList(saveData[1].split(","))); - // we really don't need to load the inventory. - } else { - clientPlayer.findStartPos(World.levels[World.lvlIdx(0)]); // find a new start pos - // this is a new player. - playerdata = clientPlayer.getPlayerData(); - // save the new player once, immediately. - serverThread.writeClientSave(playerdata); - } - serverThread.sendData(InputType.PLAYER, playerdata); - - // now, we send the INIT_W packet and notify the others clients. - - int playerlvl = World.lvlIdx(clientPlayer.getLevel() != null ? clientPlayer.getLevel().depth : 0); - if (!Arrays.asList(World.levels[playerlvl].getEntityArray()).contains(clientPlayer) - && clientPlayer != hostPlayer) // this will be true if their file was already found, since they are - // added in Load.loadPlayer(). - World.levels[playerlvl].add(clientPlayer); // add to level (**id is generated here**) and also, maybe, - // broadcasted to other players? - - updateGameVars(); - // making INIT_W packet - int[] toSend = { clientPlayer.eid, World.levels[playerlvl].w, World.levels[playerlvl].h, playerlvl, // these - // bottom - // three - // are - // actually - // unnecessary - // because - // of - // the - // previous - // PLAYER - // packet. - clientPlayer.x, clientPlayer.y }; - StringBuilder sendString = new StringBuilder(); - for (int val : toSend) - sendString.append(val).append(","); - // send client world info - if (Game.debug) - System.out.println("SERVER: Sending INIT packet"); - serverThread.sendData(InputType.INIT, sendString.toString()); - return true; - - case LOAD: - if (Game.debug) - System.out.println("SERVER: Received level data request"); - - // send back the tiles in the level specified. - int levelidx = Integer.parseInt(alldata); - - if (levelidx < 0 || levelidx >= World.levels.length) { - System.err.println("SERVER warning: Client " + clientPlayer - + " tried to request tiles from nonexistent level " + levelidx); - serverThread.sendError("Requested level (" + levelidx + ") does not exist."); - return false; - } - - // if it's the same level, it will cancel out. - - byte[] tiledata = new byte[World.levels[levelidx].tiles.length * 2]; - for (int i = 0; i < tiledata.length / 2 - 1; i++) { - tiledata[i * 2] = World.levels[levelidx].tiles[i]; - tiledata[i * 2 + 1] = World.levels[levelidx].data[i]; - } - serverThread.cachePacketTypes(InputType.tileUpdates); - - StringBuilder tiledataString = new StringBuilder(); - for (byte b : tiledata) { - tiledataString.append(b).append(","); - } - serverThread.sendData(InputType.TILES, tiledataString.substring(0, tiledataString.length() - 1)); - serverThread.sendCachedPackets(); - - // move the associated player to the level they requested -- they shouldn't be - // requesting it if they aren't going to transfer to it. - // moved to after the tile data is sent so that the client doesn't try to add - // anything to the level before it gets created. - Level next = World.levels[levelidx]; - if (clientPlayer.getLevel() != null && !next.getTile(clientPlayer.x >> 4, clientPlayer.y >> 4).mayPass(next, - clientPlayer.x >> 4, clientPlayer.y >> 4, clientPlayer)) - clientPlayer.findStartPos(next, false); - - next.add(clientPlayer); - - // send back the entities in the level specified. - - Entity[] entities = World.levels[levelidx].getEntityArray(); - serverThread.cachePacketTypes(InputType.entityUpdates); - - StringBuilder edata = new StringBuilder(); - for (Entity curEntity : entities) { - if (!clientPlayer.shouldTrack(curEntity.x >> 4, curEntity.y >> 4, curEntity.getLevel())) - continue; // this is outside of the player's entity tracking range; doesn't need to know - // about it yet. - String curEntityData = ""; - if (curEntity != clientPlayer) { - curEntityData = Save.writeEntity(curEntity, false) + ","; - if (Game.debug && curEntity instanceof Player) - System.out.println("SERVER: Sending player in ENTITIES packet: " + curEntity); - } - // there is enough space. - if (curEntityData.length() > 1) // 1 b/c of the "," added; this prevents entities that aren't saved from - // causing ",," to appear. - edata.append(curEntityData); - } - - String edataToSend = edata.substring(0, Math.max(0, edata.length() - 1)); // cut off trailing comma - - serverThread.sendData(InputType.ENTITIES, edataToSend); - serverThread.sendCachedPackets(); - - return true; - - case DIE: - if (Game.debug) - System.out.println("Received player death"); - Load.loadEntity(alldata, false); - broadcastEntityRemoval(clientPlayer, true); - return true; - - case RESPAWN: - serverThread.respawnPlayer(); - broadcastEntityAddition(clientPlayer); - // added player; client will request level data when it's ready - return true; - - case DISCONNECT: - serverThread.endConnection(); - return true; - - case DROP: - Item dropped = Items.get(alldata); - Level playerLevel = clientPlayer.getLevel(); - if (playerLevel != null) - playerLevel.dropItem(clientPlayer.x, clientPlayer.y, dropped); - return true; - - case TILE: - int lvlDepth = Integer.parseInt(data[0]); - int xt = Integer.parseInt(data[1]); - int yt = Integer.parseInt(data[2]); - for (int lvly = yt - 1; lvly <= yt + 1; lvly++) - for (int lvlx = xt - 1; lvlx <= xt + 1; lvlx++) - serverThread.sendTileUpdate(lvlDepth, lvlx, lvly); - return true; - - case ENTITY: - // client wants the specified entity sent in an ADD packet, becuase it couldn't - // find that entity upon receiving an ENTITY packet from the server. - int enid = Integer.parseInt(alldata); - Entity entityToSend = Network.getEntity(enid); - if (entityToSend == null) { - /// well THIS would be a problem, I think. Though... Actually, not really. It - /// just means that an entity was removed between the time of sending an update - /// for it, and the client then asking for it to be added. But since it would be - /// useless to add it at this point, we'll just ignore the request. - if (Game.debug) - System.out.println( - "SERVER: Ignoring request to add unknown entity (probably already removed): " + enid); - return false; - } - - if (!clientPlayer.shouldSync(entityToSend.x >> 4, entityToSend.y >> 4, entityToSend.getLevel())) { - // the requested entity is not even in range - return false; - } - if (Game.debug) - System.out.println("SERVER: Sending entity addition via " + serverThread - + " because client requested it: " + entityToSend); - serverThread.sendEntityAddition(entityToSend); - return true; - - case SAVE: - if (Game.debug) - System.out.println("SERVER: Received player save from " + serverThread.getClient()); - // save this client's data to a file. - // first, determine if this is the main player. if not, determine if a file - // already exists for this client. if not, find an available file name. for - // simplicity, we will just count the number of remote player files saved. - - if (clientPlayer == hostPlayer) { - if (Game.debug) - System.out.println("SERVER: Identified SAVE packet client as host"); - String[] parts = alldata.split("\\n"); - List datastrs = new ArrayList<>(); - - Save save = new Save(clientPlayer, false); - datastrs.addAll(Arrays.asList(parts[1].split(","))); - save.writeToFile(save.location + "Player" + Save.extension, datastrs); - datastrs.clear(); - datastrs.addAll(Arrays.asList(parts[2].split(","))); - save.writeToFile(save.location + "Inventory" + Save.extension, datastrs); - - return true; - } - - serverThread.writeClientSave(alldata); // writes the data in a RemotePlayer save file. - - return true; - - case CHESTIN: - case CHESTOUT: - int eid = Integer.parseInt(data[0]); - Entity e = Network.getEntity(eid); - if (!(e instanceof Chest)) { - System.err.println( - "SERVER: Error with CHESTOUT request: Specified chest entity did not exist or was not a chest."); - return false; - } - - Chest chest = (Chest) e; - - if (e instanceof DeathChest) { - StringBuilder itemDataB = new StringBuilder(); - for (Item i : chest.getInventory().getItems()) - itemDataB.append(i.getData()).append(";"); - String itemData = itemDataB.toString(); - itemData = itemData.length() == 0 ? itemData : itemData.substring(0, itemData.length() - 1); - serverThread.sendItems(itemData); - serverThread.sendNotification("Death chest retrieved!", 0); - chest.remove(); - return true; - } - - int itemIdx = Integer.parseInt(data[1]); - - if (inType == InputType.CHESTIN) { - Item item = Items.get(data[2]); - if (item instanceof UnknownItem) { - System.err.println( - "SERVER error with CHESTIN request: Specified item could not be found from string: " - + data[2]); - return false; - } - if (itemIdx > chest.getInventory().invSize()) - itemIdx = chest.getInventory().invSize(); - chest.getInventory().add(itemIdx, item); - } else { /// inType == InputType.CHESTOUT - - if (itemIdx >= chest.getInventory().invSize() || itemIdx < 0) { - System.err - .println("SERVER: Error with CHESTOUT request: Specified chest inv index is out of bounds: " - + itemIdx + "; inv size:" + chest.getInventory().invSize()); - return false; - } - // if here, the index is valid - boolean wholeStack = Boolean.parseBoolean(data[2]); - Item toRemove = chest.getInventory().get(itemIdx); - Item itemToSend = toRemove.clone(); - if (!wholeStack && toRemove instanceof StackableItem && ((StackableItem) toRemove).count > 1) { - ((StackableItem) itemToSend).count = 1; - ((StackableItem) toRemove).count--; - } else - chest.getInventory().remove(itemIdx); - - serverThread.sendData(InputType.CHESTOUT, itemToSend.getData() + ";" + data[3]); // send back the item - // that the player - // should put in their - // inventory. - } - - serverThread.sendEntityUpdate(chest, chest.getUpdates()); - return true; - - case PUSH: - int furnitureID = Integer.parseInt(alldata); - Entity furniture = Network.getEntity(furnitureID); - if (furniture == null) { - System.err.println("SERVER: Couldn't find the specified piece of furniture to push: " + furnitureID); - return false; - } else if (!(furniture instanceof Furniture)) { - System.err.println("SERVER: Specified entity is not an instance of the furniture class: " + furniture - + "; cannot push."); - return false; - } - - ((Furniture) furniture).tryPush(clientPlayer); - return true; - - case PICKUP: - int ieid = Integer.parseInt(alldata); - Entity entity = Network.getEntity(ieid); - if (!(entity instanceof ItemEntity) || entity.isRemoved()) { - System.err.println("SERVER: Could not find item entity in pickup request " + ieid - + ". Telling client to remove..."); - serverThread.sendEntityRemoval(ieid); // will happen when another guy gets to it first, so the this - // client shouldn't have it on the level anymore. It could also - // happen if the client didn't receive the packet telling them to - // pick it up... in which case it will be lost, but oh well. - return false; - } - - entity.remove(); - serverThread.sendData(inType, alldata); - broadcastData(InputType.REMOVE, String.valueOf(entity.eid), serverThread); - return true; - - case INTERACT: - clientPlayer.activeItem = Items.get(data[0], true); // this can be null; and that's fine, it means a fist. - // ;) - clientPlayer.stamina = Integer.parseInt(data[1]); - int arrowCount = Integer.parseInt(data[2]); - int curArrows = clientPlayer.getInventory().count(Items.arrowItem); - if (curArrows < arrowCount) - clientPlayer.getInventory().add(Items.arrowItem, arrowCount - curArrows); - if (curArrows > arrowCount) - clientPlayer.getInventory().removeItems(Items.arrowItem, curArrows - arrowCount); - clientPlayer.attack(); /// NOTE the player may fire an arrow, but we won't sync the arrow count because - /// that player will update it theirself. - - serverThread.sendData(InputType.INTERACT, - (clientPlayer.activeItem == null ? "null" : clientPlayer.activeItem.getData())); - return true; - - case BED: - if (Game.debug) - System.out.println("Received bed request: " + alldata); - boolean getIn = Boolean.parseBoolean(data[0]); - if (getIn) { - Entity bed = Network.getEntity(Integer.parseInt(data[1])); - if (!(bed instanceof Bed) || !Bed.checkCanSleep(clientPlayer)) { - updateGameVars(); - return false; - } - - ((Bed) bed).use(clientPlayer); - } else { - if (Bed.sleeping()) - return false; // can't quit once everyone is in bed - // else, get the player out of bed - Bed.restorePlayer(clientPlayer); - } - return true; - - case POTION: - boolean addEffect = Boolean.parseBoolean(data[0]); - int typeIdx = Integer.parseInt(data[1]); - PotionItem.applyPotion(clientPlayer, PotionType.values[typeIdx], addEffect); - return true; - - case SHIRT: - clientPlayer.shirtColor = Integer.parseInt(alldata); - broadcastEntityUpdate(clientPlayer, false); - return true; - - case PLAYER: - clientPlayer.update(alldata); - return true; - - case MOVE: - // the player moved. - int plvlidx = Integer.parseInt(data[3]); - if (plvlidx >= 0 && plvlidx < World.levels.length && World.levels[plvlidx] != clientPlayer.getLevel()) { - clientPlayer.remove(); - World.levels[plvlidx].add(clientPlayer); - } - - int oldx = clientPlayer.x >> 4, oldy = clientPlayer.y >> 4; - int newx = Integer.parseInt(data[0]); - int newy = Integer.parseInt(data[1]); - - boolean moved = clientPlayer.move(newx - clientPlayer.x, newy - clientPlayer.y); // this moves the player, - // and updates other - // clients. - - clientPlayer.dir = Direction.values[Integer.parseInt(data[2])]; // do this AFTERWARD, so that the move - // method doesn't mess something up. - - if (moved) - clientPlayer.updateSyncArea(oldx, oldy); // this updates the current client. - - broadcastEntityUpdate(clientPlayer, !moved); // this will make it so that if the player is prevented from - // moving, the server will update the client, forcing it back - // to the last place the server recorded the player at. TODO - // this breaks down with a slow connection... - clientPlayer.walkDist++; // hopefully will make walking animations work. Actually, they should be sent - // with Mob's update... no, it doesn't update, it just feeds back. - - return true; - - // I'm thinking this should end up never being used... oh, well maybe for - // notifications, actually. - default: - System.out.println("SERVER: Used default behavior for input type " + inType); - broadcastData(inType, alldata, serverThread); - return true; - } - } - - private void broadcastData(InputType inType, String data) { - broadcastData(inType, data, (MinicraftServerThread) null); - } - - private void broadcastData(InputType inType, String data, @Nullable MinicraftServerThread clientThreadToExclude) { - for (MinicraftServerThread thread : getThreads()) { - if (thread != clientThreadToExclude) // send this packet to all EXCEPT the specified one. - thread.sendData(inType, data); - } - } - - @SuppressWarnings("unused") - private void broadcastData(InputType inType, String data, List threads) { - for (MinicraftServerThread thread : threads) - thread.sendData(inType, data); - } - - protected synchronized void onThreadDisconnect(MinicraftServerThread thread) { - threadList.remove(thread); - if (thread.getClient() == hostPlayer) - hostPlayer = null; - } - - @Override - public synchronized void endConnection() { - if (!isConnected()) - return; - - if (Game.debug) - System.out.println("SERVER: Ending connection with threads: " + threadList); - - if (hasClients()) { - broadcastData(InputType.SAVE, ""); - MyUtils.sleep(1000); // give time for the clients to send back their player data - - MinicraftServerThread[] threads = getThreads(); - for (MinicraftServerThread thread : threads) - thread.endConnection(); - } - - try { - socket.close(); - } catch (IOException ignored) { - } - - threadList.clear(); // should already be clear - } - - @Override - public boolean isConnected() { - return socket != null && !socket.isClosed(); - } - - public boolean hasClients() { - return threadList.size() > 0; - } + + class MyTask extends TimerTask { + public MyTask() {} + public void run() {} + } + + private static final int UPDATE_INTERVAL = 10; // measured in seconds + + private final int port; + + private List threadList = Collections.synchronizedList(new ArrayList<>()); + private ServerSocket socket; + + private RemotePlayer hostPlayer = null; + private String worldPath; + + private int playerCap = 5; + + public MinicraftServer(int port) { + super("MinicraftServer"); + + this.port = port; + + Game.ISONLINE = true; + Game.ISHOST = true; // just in case. + Game.player.remove(); // the server has no player... + + worldPath = Game.gameDir + "/saves/" + WorldSelectDisplay.getWorldName(); + + try { + System.out.println("Opening server socket..."); + socket = new ServerSocket(port); + start(); + } catch (IOException ex) { + System.err.println("Failed to open server socket on port " + port); + ex.printStackTrace(); + } + } + + public void run() { + if (Game.debug) System.out.println("Server started."); + + Timer gameUpdateTimer = new Timer("GameUpdateTimer"); + gameUpdateTimer.schedule(new MyTask() { + public void run() { updateGameVars(); } + }, 5000, UPDATE_INTERVAL*1000); + + try { + while (socket != null) { + MinicraftServerThread mst = new MinicraftServerThread(socket.accept(), this); + if (mst.isConnected()) + threadList.add(mst); + } + } catch (SocketException ex) { // this should occur when closing the thread. + } catch (IOException ex) { + System.err.println("Server socket encountered an error while attempting to listen on port " + port + ":"); + ex.printStackTrace(); + } + + gameUpdateTimer.cancel(); + System.out.println("Closing server socket"); + + endConnection(); + } + + public String getWorldPath() { return worldPath; } + + public int getPlayerCap() { return playerCap; } + public void setPlayerCap(int val) { + playerCap = Math.max(val, -1); // no need to set it to anything below -1. + } + + public boolean isFull() { + return playerCap >= 0 && threadList.size() >= playerCap; + } + + public int getNumPlayers() { return threadList.size(); } + + private MinicraftServerThread[] getThreads() { + return threadList.toArray(new MinicraftServerThread[threadList.size()]); + } + + public String[] getClientInfo() { + List playerStrings = new ArrayList<>(); + for (MinicraftServerThread serverThread: getThreads()) { + RemotePlayer clientPlayer = serverThread.getClient(); + + playerStrings.add(clientPlayer.getUsername() + ": " + clientPlayer.getIpAddress().getHostAddress() + (Game.debug?" (" +(clientPlayer.x>>4)+ "," +(clientPlayer.y>>4)+ ")":"")); + } + + return playerStrings.toArray(new String[0]); + } + + public List getPlayersInRange(Entity e, boolean useTrackRange) { + if (e == null || e.getLevel() == null) return new ArrayList<>(); + int xt = e.x >> 4, yt = e.y >> 4; + return getPlayersInRange(e.getLevel(), xt, yt, useTrackRange); // NOTE if "e" is a RemotePlayer, the list returned *will* contain "e". + } + public List getPlayersInRange(Level level, int xt, int yt, boolean useTrackRange) { + List players = new ArrayList<>(); + for (MinicraftServerThread thread: getThreads()) { + RemotePlayer rp = thread.getClient(); + if (useTrackRange && rp.shouldTrack(xt, yt, level) || !useTrackRange && rp.shouldSync(xt, yt, level)) + players.add(rp); + } + + return players; + } + + @Nullable + private RemotePlayer getIfPlayer(Entity e) { + if (e instanceof RemotePlayer) { + return (RemotePlayer) e; + } + else + return null; + } + + @Nullable + public MinicraftServerThread getAssociatedThread(String username) { + MinicraftServerThread match = null; + + for (MinicraftServerThread thread: getThreads()) { + if (thread.getClient().getUsername().equalsIgnoreCase(username)) { + match = thread; + break; + } + } + + return match; + } + + public List getAssociatedThreads(String[] usernames, boolean printError) { + List threads = new ArrayList<>(); + for (String username: usernames) { + MinicraftServerThread match = getAssociatedThread(username); + if (match != null) + threads.add(match); + else if (printError) + System.err.println("Couldn't match username \"" + username + "\""); + } + + return threads; + } + + @NotNull + public MinicraftServerThread getAssociatedThread(RemotePlayer player) { + MinicraftServerThread thread = null; + + for (MinicraftServerThread curThread: getThreads()) { + if (curThread.getClient() == player) { + thread = curThread; + break; + } + } + + if (thread == null) { + System.err.println("SERVER: Could not find thread for remote player " + player); + thread = new MinicraftServerThread(player, this); + } + + return thread; + } + + private List getAssociatedThreads(List players) { + List threads = new ArrayList<>(); + + // NOTE I could do this the other way around, by looping though the thread list, and adding those whose player is found in the given list, which might be slightly more optimal... but I think it's better that this tells you when a player in the list doesn't have a matching thread. + for (RemotePlayer client: players) { + MinicraftServerThread thread = getAssociatedThread(client); + if (thread.isValid()) + threads.add(thread); + else + System.err.println("SERVER WARNING: Couldn't find server thread for client " + client); + } + + return threads; + } + + public void broadcastEntityUpdate(Entity e) { broadcastEntityUpdate(e, false); } + public void broadcastEntityUpdate(Entity e, boolean updateSelf) { + if (e.isRemoved()) { + if (Game.debug) System.out.println("SERVER: Tried to broadcast update of removed entity: " + e); + return; + } + List players = getPlayersInRange(e, false); + if (!updateSelf) { + players.remove(getIfPlayer(e)); + } + + for (MinicraftServerThread thread: getAssociatedThreads(players)) { + thread.sendEntityUpdate(e, e.getUpdates()); + } + + e.flushUpdates(); // It is important that this method is only called once: right here. + } + + public void broadcastTileUpdate(Level level, int x, int y) { + broadcastData(InputType.TILE, Tile.getData(level.depth, x, y)); + } + + public void broadcastEntityAddition(Entity e) { broadcastEntityAddition(e, false); } + public void broadcastEntityAddition(Entity e, boolean addSelf) { + if (e.isRemoved()) { + if (Game.debug) System.out.println("SERVER: Tried to broadcast addition of removed entity: " + e); + return; + } + List players = getPlayersInRange(e, true); + if (!addSelf) + players.remove(getIfPlayer(e)); // if "e" is a player, this removes it from the list. + int cnt = 0; + if (Game.debug && e instanceof Player) System.out.println("SERVER: Broadcasting player addition of " + e); + for (MinicraftServerThread thread: getAssociatedThreads(players)) { + thread.sendEntityAddition(e); + cnt++; + } + if (Game.debug && e instanceof Player) System.out.println("SERVER: Broadcasted player addition of " + e + " to " + cnt + " clients"); + } + + private List getPlayersToRemove(Entity e, boolean removeSelf) { + List players = getPlayersInRange(e, true); + if (players.size() == 0) return players; + if (Game.debug && e instanceof Player) { + System.out.println("SERVER: Sending removal of player " + e + " to " + players.size() + " players (may remove equal player): "); + for (RemotePlayer rp: players) + System.out.println(rp); + } + + if (!removeSelf) + players.remove(getIfPlayer(e)); // if "e" is a player, this removes it from the list. + + if (Game.debug && e instanceof Player) System.out.println("...now sending player removal to " + players.size() + " players."); + + return players; + } + + // remove only if on given level + public void broadcastEntityRemoval(Entity e, Level level, boolean removeSelf) { + List players = getPlayersToRemove(e, removeSelf); + + if (level == null) { + if (Game.debug) System.out.println("SERVER: Cannot remove entity " +e+ " from specified level, level given is null; ignoring request to broadcast entity removal."); + return; + } + + for (MinicraftServerThread thread: getAssociatedThreads(players)) + thread.sendEntityRemoval(e.eid, level.depth); + } + // remove regardless of level + public void broadcastEntityRemoval(Entity e, boolean removeSelf) { + List players = getPlayersToRemove(e, removeSelf); + + for (MinicraftServerThread thread: getAssociatedThreads(players)) + thread.sendEntityRemoval(e.eid); + } + + public void saveWorld() { + broadcastData(InputType.SAVE, ""); // tell all the other clients to send their data over to be saved. + new Save(WorldSelectDisplay.getWorldName()); + System.out.println("World saved"); + } + + public void broadcastNotification(String note, int notetime) { + String data = notetime + ";" + note; + broadcastData(InputType.NOTIFY, data); + } + + public void broadcastPlayerHurt(int eid, int damage, Direction attackDir) { + for (MinicraftServerThread thread: getThreads()) + thread.sendPlayerHurt(eid, damage, attackDir); + } + + public void broadcastStopFishing(int eid) { + for (MinicraftServerThread thread: getThreads()) + thread.sendStopFishing(eid); + } + + public void updateGameVars() { updateGameVars(getThreads()); } + public void updateGameVars(MinicraftServerThread sendTo) { + updateGameVars(new MinicraftServerThread[] {sendTo}); + } + public void updateGameVars(MinicraftServerThread[] sendTo) { + //if (Game.debug) System.out.println("SERVER: updating game vars..."); + if (sendTo.length == 0) return; + + String[] varArray = { + Settings.get("mode").toString(), + Updater.tickCount+ "", + Updater.gamespeed+ "", + Updater.pastDay1+ "", + Updater.scoreTime+ "", + getNumPlayers()+ "", + Bed.getPlayersAwake()+ "" + }; + + String vars = String.join(";", varArray); + + for (MinicraftServerThread thread: sendTo) + thread.sendData(InputType.GAME, vars); + } + + public void pingClients() { + System.out.println("Pinging clients (" +threadList.size()+ " connected)..."); + for (MinicraftServerThread thread: getThreads()) + thread.doPing(); + } + + protected File[] getRemotePlayerFiles() { + File saveFolder = new File(worldPath); + + File[] clientSaves = saveFolder.listFiles((file, name) -> name.startsWith("RemotePlayer")); + + if (clientSaves == null) + clientSaves = new File[0]; + + return clientSaves; + } + + boolean parsePacket(MinicraftServerThread serverThread, InputType inType, String alldata) { + String[] data = alldata.split(";"); + + RemotePlayer clientPlayer = serverThread.getClient(); + if (clientPlayer == null) { + System.err.println("CRITICAL SERVER ERROR: Server thread client is null: " + serverThread + "; cannot parse the received " +inType+ " packet: " + alldata); + return false; + } + + // handle reports of type INVALID + if (inType == InputType.INVALID) { + if (Game.debug) System.out.println(serverThread + " received an error:"); + System.err.println(alldata); + return false; + } + + if (InputType.serverOnly.contains(inType)) { + /// these are ALL illegal for a client to send. + System.err.println("SERVER WARNING: Client " + clientPlayer + " sent illegal packet type " + inType + " to server."); + return false; + } + + switch(inType) { + case PING: + System.out.println("Received ping from " + serverThread); + return true; + + case LOGIN: + if (Game.debug) System.out.println("SERVER: Received login request"); + if (Game.debug) System.out.println("SERVER: Login data: " + Arrays.toString(data)); + String username = data[0]; + Version clientVersion = new Version(data[1]); + // check version; send back invalid if they don't match. + if (clientVersion.compareTo(Game.VERSION) != 0) { + serverThread.sendError("Wrong game version; need " + Game.VERSION); + return false; + } + + // check if the same username already exists on the server (due to signing on a second time with the same account), and if so, prevent the new login + if (getAssociatedThread(username) != null) { + serverThread.sendError("Account is already logged in to server"); + return false; + } + + // versions match, and username is unique; make client player + clientPlayer.setUsername(username); + + // now, we need to check if this player has played in this world before. If they have, then all previous settings and items and such will be restored. + String playerdata = ""; // this stores the data fetched from the files. + + if (serverThread.getClient().getIpAddress().isLoopbackAddress() && hostPlayer == null) { + // this is the first person on localhost. I believe that a second person will be saved as a loopback address (later: jk the first one actually will), but a third will simply overwrite the second. + if (Game.debug) System.out.println("SERVER: Host player found"); + + if (Game.player != null) { + // save the player, and then remove it. It is leftover from when this was a single player world. + playerdata = Game.player.getPlayerData(); + Game.player.remove(); // all the important data has been saved. + Game.player = null; + } else { + // load the data from file instead. + playerdata = Game.VERSION+ "\n"; + try { + playerdata += Load.loadFromFile(worldPath+ "/Player" +Save.extension, true) + "\n"; + playerdata += Load.loadFromFile(worldPath+ "/Inventory" +Save.extension, true); + } catch(IOException ex) { + System.err.println("SERVER: Server had error while trying to load host player data from file:"); + ex.printStackTrace(); + } + } + + hostPlayer = clientPlayer; + } + else { + playerdata = serverThread.getRemotePlayerFileData(); + } + + if (playerdata.length() > 0) { + // if a save file was found, then send the data to the client so they can resume where they left off. + // and now, initialize the RemotePlayer instance with the data. + // first get the version that saved the file, which can be different if this is a remote player. + String[] saveData = playerdata.split("\\n"); + new Load(new Version(saveData[0])).loadPlayer(clientPlayer, Arrays.asList(saveData[1].split(","))); + // we really don't need to load the inventory. + } else { + clientPlayer.findStartPos(World.levels[World.lvlIdx(0)]); // find a new start pos + // this is a new player. + playerdata = clientPlayer.getPlayerData(); + // save the new player once, immediately. + serverThread.writeClientSave(playerdata); + } + serverThread.sendData(InputType.PLAYER, playerdata); + + // now, we send the INIT_W packet and notify the others clients. + + int playerlvl = World.lvlIdx(clientPlayer.getLevel() != null ? clientPlayer.getLevel().depth : 0); + if (!Arrays.asList(World.levels[playerlvl].getEntityArray()).contains(clientPlayer) && clientPlayer != hostPlayer) // this will be true if their file was already found, since they are added in Load.loadPlayer(). + World.levels[playerlvl].add(clientPlayer); // add to level (**id is generated here**) and also, maybe, broadcasted to other players? + + updateGameVars(); + //making INIT_W packet + int[] toSend = { + clientPlayer.eid, + World.levels[playerlvl].w, + World.levels[playerlvl].h, + playerlvl, // these bottom three are actually unnecessary because of the previous PLAYER packet. + clientPlayer.x, + clientPlayer.y + }; + StringBuilder sendString = new StringBuilder(); + for (int val: toSend) + sendString.append(val).append(","); + // send client world info + if (Game.debug) System.out.println("SERVER: Sending INIT packet"); + serverThread.sendData(InputType.INIT, sendString.toString()); + return true; + + case LOAD: + if (Game.debug) System.out.println("SERVER: Received level data request"); + // send back the tiles in the level specified. + int levelidx = Integer.parseInt(alldata); + if (levelidx < 0 || levelidx >= World.levels.length) { + System.err.println("SERVER warning: Client " + clientPlayer + " tried to request tiles from nonexistent level " + levelidx); + serverThread.sendError("Requested level (" +levelidx+ ") does not exist."); + return false; + } + + // if it's the same level, it will cancel out. + + byte[] tiledata = new byte[World.levels[levelidx].tiles.length*2]; + for (int i = 0; i < tiledata.length/2 - 1; i++) { + tiledata[i*2] = World.levels[levelidx].tiles[i]; + tiledata[i*2+1] = World.levels[levelidx].data[i]; + } + serverThread.cachePacketTypes(InputType.tileUpdates); + + StringBuilder tiledataString = new StringBuilder(); + for (byte b: tiledata) { + tiledataString.append(b).append(","); + } + serverThread.sendData(InputType.TILES, tiledataString.substring(0, tiledataString.length()-1)); + serverThread.sendCachedPackets(); + + // move the associated player to the level they requested -- they shouldn't be requesting it if they aren't going to transfer to it. + // moved to after the tile data is sent so that the client doesn't try to add anything to the level before it gets created. + Level next = World.levels[levelidx]; + if (clientPlayer.getLevel() != null && !next.getTile(clientPlayer.x >> 4, clientPlayer.y >> 4).mayPass(next, clientPlayer.x >> 4, clientPlayer.y >> 4, clientPlayer)) + clientPlayer.findStartPos(next, false); + + next.add(clientPlayer); + + // send back the entities in the level specified. + + Entity[] entities = World.levels[levelidx].getEntityArray(); + serverThread.cachePacketTypes(InputType.entityUpdates); + + StringBuilder edata = new StringBuilder(); + for (Entity curEntity : entities) { + if (!clientPlayer.shouldTrack(curEntity.x >> 4, curEntity.y >> 4, curEntity.getLevel())) + continue; // this is outside of the player's entity tracking range; doesn't need to know about it yet. + String curEntityData = ""; + if (curEntity != clientPlayer) { + curEntityData = Save.writeEntity(curEntity, false) + ","; + if (Game.debug && curEntity instanceof Player) + System.out.println("SERVER: Sending player in ENTITIES packet: " + curEntity); + } + // there is enough space. + if (curEntityData.length() > 1) // 1 b/c of the "," added; this prevents entities that aren't saved from causing ",," to appear. + edata.append(curEntityData); + } + + String edataToSend = edata.substring(0, Math.max(0, edata.length()-1)); // cut off trailing comma + + serverThread.sendData(InputType.ENTITIES, edataToSend); + serverThread.sendCachedPackets(); + + return true; + + case DIE: + if (Game.debug) System.out.println("Received player death"); + Load.loadEntity(alldata, false); + broadcastEntityRemoval(clientPlayer, true); + return true; + + case RESPAWN: + serverThread.respawnPlayer(); + broadcastEntityAddition(clientPlayer); + // added player; client will request level data when it's ready + return true; + + case DISCONNECT: + serverThread.endConnection(); + return true; + + case DROP: + Item dropped = Items.get(alldata); + Level playerLevel = clientPlayer.getLevel(); + if (playerLevel != null) + playerLevel.dropItem(clientPlayer.x, clientPlayer.y, dropped); + return true; + + case TILE: + int lvlDepth = Integer.parseInt(data[0]); + int xt = Integer.parseInt(data[1]); + int yt = Integer.parseInt(data[2]); + for (int lvly = yt-1; lvly <= yt+1; lvly++) + for (int lvlx = xt-1; lvlx <= xt+1; lvlx++) + serverThread.sendTileUpdate(lvlDepth, lvlx, lvly); + return true; + + case ENTITY: + // client wants the specified entity sent in an ADD packet, becuase it couldn't find that entity upon receiving an ENTITY packet from the server. + int enid = Integer.parseInt(alldata); + Entity entityToSend = Network.getEntity(enid); + if (entityToSend == null) { + /// well THIS would be a problem, I think. Though... Actually, not really. It just means that an entity was removed between the time of sending an update for it, and the client then asking for it to be added. But since it would be useless to add it at this point, we'll just ignore the request. + if (Game.debug) System.out.println("SERVER: Ignoring request to add unknown entity (probably already removed): " + enid); + return false; + } + + if (!clientPlayer.shouldSync(entityToSend.x >> 4, entityToSend.y >> 4, entityToSend.getLevel())) { + // the requested entity is not even in range + return false; + } + if (Game.debug) System.out.println("SERVER: Sending entity addition via " + serverThread + " because client requested it: " + entityToSend); + serverThread.sendEntityAddition(entityToSend); + return true; + + case SAVE: + if (Game.debug) System.out.println("SERVER: Received player save from " + serverThread.getClient()); + // save this client's data to a file. + // first, determine if this is the main player. if not, determine if a file already exists for this client. if not, find an available file name. for simplicity, we will just count the number of remote player files saved. + + if (clientPlayer == hostPlayer) { + if (Game.debug) System.out.println("SERVER: Identified SAVE packet client as host"); + String[] parts = alldata.split("\\n"); + List datastrs = new ArrayList<>(); + + Save save = new Save(clientPlayer, false); + datastrs.addAll(Arrays.asList(parts[1].split(","))); + save.writeToFile(save.location+ "Player" +Save.extension, datastrs); + datastrs.clear(); + datastrs.addAll(Arrays.asList(parts[2].split(","))); + save.writeToFile(save.location+ "Inventory" +Save.extension, datastrs); + + return true; + } + + serverThread.writeClientSave(alldata); // writes the data in a RemotePlayer save file. + + return true; + + case CHESTIN: case CHESTOUT: + int eid = Integer.parseInt(data[0]); + Entity e = Network.getEntity(eid); + if (!(e instanceof Chest)) { + System.err.println("SERVER: Error with CHESTOUT request: Specified chest entity did not exist or was not a chest."); + return false; + } + + Chest chest = (Chest) e; + + if (e instanceof DeathChest) { + StringBuilder itemDataB = new StringBuilder(); + for (Item i: chest.getInventory().getItems()) + itemDataB.append(i.getData()).append(";"); + String itemData = itemDataB.toString(); + itemData = itemData.length() == 0 ? itemData : itemData.substring(0, itemData.length() - 1); + serverThread.sendItems(itemData); + serverThread.sendNotification("Death chest retrieved!", 0); + chest.remove(); + return true; + } + + int itemIdx = Integer.parseInt(data[1]); + + if (inType == InputType.CHESTIN) { + Item item = Items.get(data[2]); + if (item instanceof UnknownItem) { + System.err.println("SERVER error with CHESTIN request: Specified item could not be found from string: " + data[2]); + return false; + } + if (itemIdx > chest.getInventory().invSize()) + itemIdx = chest.getInventory().invSize(); + chest.getInventory().add(itemIdx, item); + } + else { /// inType == InputType.CHESTOUT + + if (itemIdx >= chest.getInventory().invSize() || itemIdx < 0) { + System.err.println("SERVER: Error with CHESTOUT request: Specified chest inv index is out of bounds: " + itemIdx + "; inv size:" + chest.getInventory().invSize()); + return false; + } + // if here, the index is valid + boolean wholeStack = Boolean.parseBoolean(data[2]); + Item toRemove = chest.getInventory().get(itemIdx); + Item itemToSend = toRemove.clone(); + if (!wholeStack && toRemove instanceof StackableItem && ((StackableItem)toRemove).count > 1) { + ((StackableItem)itemToSend).count = 1; + ((StackableItem)toRemove).count--; + } else + chest.getInventory().remove(itemIdx); + + serverThread.sendData(InputType.CHESTOUT, itemToSend.getData()+ ";" +data[3]); // send back the item that the player should put in their inventory. + } + + serverThread.sendEntityUpdate(chest, chest.getUpdates()); + return true; + + case PUSH: + int furnitureID = Integer.parseInt(alldata); + Entity furniture = Network.getEntity(furnitureID); + if (furniture == null) { + System.err.println("SERVER: Couldn't find the specified piece of furniture to push: " + furnitureID); + return false; + } else if (!(furniture instanceof Furniture)) { + System.err.println("SERVER: Specified entity is not an instance of the furniture class: " + furniture + "; cannot push."); + return false; + } + + ((Furniture)furniture).tryPush(clientPlayer); + return true; + + case PICKUP: + int ieid = Integer.parseInt(alldata); + Entity entity = Network.getEntity(ieid); + if (!(entity instanceof ItemEntity) || entity.isRemoved()) { + System.err.println("SERVER: Could not find item entity in pickup request " + ieid + ". Telling client to remove..."); + serverThread.sendEntityRemoval(ieid); // will happen when another guy gets to it first, so the this client shouldn't have it on the level anymore. It could also happen if the client didn't receive the packet telling them to pick it up... in which case it will be lost, but oh well. + return false; + } + + entity.remove(); + serverThread.sendData(inType, alldata); + broadcastData(InputType.REMOVE, String.valueOf(entity.eid), serverThread); + return true; + + case INTERACT: + clientPlayer.activeItem = Items.get(data[0], true); // this can be null; and that's fine, it means a fist. ;) + clientPlayer.stamina = Integer.parseInt(data[1]); + int arrowCount = Integer.parseInt(data[2]); + int curArrows = clientPlayer.getInventory().count(Items.arrowItem); + if (curArrows < arrowCount) + clientPlayer.getInventory().add(Items.arrowItem, arrowCount-curArrows); + if (curArrows > arrowCount) + clientPlayer.getInventory().removeItems(Items.arrowItem, curArrows-arrowCount); + clientPlayer.attack(); /// NOTE the player may fire an arrow, but we won't sync the arrow count because that player will update it theirself. + + serverThread.sendData(InputType.INTERACT, ( clientPlayer.activeItem == null ? "null" : clientPlayer.activeItem.getData() )); + return true; + + case BED: + if (Game.debug) System.out.println("Received bed request: " + alldata); + boolean getIn = Boolean.parseBoolean(data[0]); + if (getIn) { + Entity bed = Network.getEntity(Integer.parseInt(data[1])); + if (!(bed instanceof Bed) || !Bed.checkCanSleep(clientPlayer)) { + updateGameVars(); + return false; + } + + ((Bed) bed).use(clientPlayer); + } + else { + if (Bed.sleeping()) return false; // can't quit once everyone is in bed + // else, get the player out of bed + Bed.restorePlayer(clientPlayer); + } + return true; + + case POTION: + boolean addEffect = Boolean.parseBoolean(data[0]); + int typeIdx = Integer.parseInt(data[1]); + PotionItem.applyPotion(clientPlayer, PotionType.values[typeIdx], addEffect); + return true; + + case SHIRT: + clientPlayer.shirtColor = Integer.parseInt(alldata); + broadcastEntityUpdate(clientPlayer, false); + return true; + + case PLAYER: + clientPlayer.update(alldata); + return true; + + case MOVE: + // the player moved. + int plvlidx = Integer.parseInt(data[3]); + if (plvlidx >= 0 && plvlidx < World.levels.length && World.levels[plvlidx] != clientPlayer.getLevel()) { + clientPlayer.remove(); + World.levels[plvlidx].add(clientPlayer); + } + + int oldx = clientPlayer.x>>4, oldy = clientPlayer.y>>4; + int newx = Integer.parseInt(data[0]); + int newy = Integer.parseInt(data[1]); + + boolean moved = clientPlayer.move(newx - clientPlayer.x, newy - clientPlayer.y); // this moves the player, and updates other clients. + + clientPlayer.dir = Direction.values[Integer.parseInt(data[2])]; // do this AFTERWARD, so that the move method doesn't mess something up. + + if (moved) clientPlayer.updateSyncArea(oldx, oldy); // this updates the current client. + + broadcastEntityUpdate(clientPlayer, !moved); // this will make it so that if the player is prevented from moving, the server will update the client, forcing it back to the last place the server recorded the player at. TODO this breaks down with a slow connection... + clientPlayer.walkDist++; // hopefully will make walking animations work. Actually, they should be sent with Mob's update... no, it doesn't update, it just feeds back. + + return true; + + // I'm thinking this should end up never being used... oh, well maybe for notifications, actually. + default: + System.out.println("SERVER: Used default behavior for input type " + inType); + broadcastData(inType, alldata, serverThread); + return true; + } + } + + private void broadcastData(InputType inType, String data) { broadcastData(inType, data, (MinicraftServerThread)null); } + private void broadcastData(InputType inType, String data, @Nullable MinicraftServerThread clientThreadToExclude) { + for (MinicraftServerThread thread: getThreads()) { + if (thread != clientThreadToExclude) // send this packet to all EXCEPT the specified one. + thread.sendData(inType, data); + } + } + private void broadcastData(InputType inType, String data, List threads) { + for (MinicraftServerThread thread: threads) + thread.sendData(inType, data); + } + + protected synchronized void onThreadDisconnect(MinicraftServerThread thread) { + threadList.remove(thread); + if (thread.getClient() == hostPlayer) + hostPlayer = null; + } + + @Override + public synchronized void endConnection() { + if (!isConnected()) return; + + if (Game.debug) System.out.println("SERVER: Ending connection with threads: " + threadList); + + if (hasClients()) { + broadcastData(InputType.SAVE, ""); + MyUtils.sleep(1000); // give time for the clients to send back their player data + + MinicraftServerThread[] threads = getThreads(); + for (MinicraftServerThread thread: threads) + thread.endConnection(); + } + + try { + socket.close(); + } catch (IOException ignored) {} + + threadList.clear(); // should already be clear + } + + @Override + public boolean isConnected() { + return socket != null && !socket.isClosed(); + } + + public boolean hasClients() { + return threadList.size() > 0; + } } \ No newline at end of file diff --git a/src/minicraft/network/MinicraftServerThread.java b/src/minicraft/network/MinicraftServerThread.java index 7886b4e9..9d3f2ae4 100644 --- a/src/minicraft/network/MinicraftServerThread.java +++ b/src/minicraft/network/MinicraftServerThread.java @@ -26,310 +26,289 @@ import minicraft.saveload.Version; public class MinicraftServerThread extends MinicraftConnection { - - private static final String autoPing = "ping"; - private static final String manualPing = "manual"; - - private static final int MISSED_PING_THRESHOLD = 5; - private static final int PING_INTERVAL = 1_000; // measured in milliseconds - - private MinicraftServer serverInstance; - private RemotePlayer client; - - /// PING - - private Timer pingTimer; - private boolean receivedPing = true; // after first pause, it will act as if the ping was successful, since it - // didn't even send one in the first place and was just buying time for - // everything to get settled before pinging. - private int missedPings = 0; - - private long manualPingTimestamp; - - private List packetTypesToKeep = new ArrayList<>(); - private List packetTypesToCache = new ArrayList<>(); - private List cachedPackets = new ArrayList<>(); - - private final boolean valid; - - MinicraftServerThread(Socket socket, MinicraftServer serverInstance) { - super("MinicraftServerThread", socket); - valid = true; - - this.serverInstance = serverInstance; - if (serverInstance.isFull()) { - sendError("server at max capacity."); - super.endConnection(); - return; - } - - client = new RemotePlayer(null, false, socket.getInetAddress(), socket.getPort()); - - // username is set later - - packetTypesToKeep.addAll(InputType.tileUpdates); - packetTypesToKeep.addAll(InputType.entityUpdates); - - pingTimer = new Timer(PING_INTERVAL, e -> { - if (!isConnected()) { - pingTimer.stop(); - return; - } - - // if(Game.debug) System.out.println("received ping from "+this+": - // "+receivedPing+". Previously missed "+missedPings+" pings."); - - if (!receivedPing) { - missedPings++; - if (missedPings >= MISSED_PING_THRESHOLD) { - // disconnect from the client; they are taking too long to respond and probably - // don't exist at this point. - pingTimer.stop(); - sendError("client ping too slow, server timed out"); - endConnection(); - } - } else { - missedPings = 0; - receivedPing = false; - } - - sendData(InputType.PING, autoPing); - }); - pingTimer.setRepeats(true); - pingTimer.setCoalesce(true); // don't try to make up for lost pings. - pingTimer.start(); - - start(); - } - - // this is to be a dummy thread. - MinicraftServerThread(RemotePlayer player, MinicraftServer server) { - super("MinicraftServerThread", null); - valid = false; - this.client = player; - this.serverInstance = server; - } - - public boolean isValid() { - return valid; - } - - public RemotePlayer getClient() { - return client; - } - - protected boolean parsePacket(InputType inType, String data) { - if (inType == InputType.PING) { - // if (Game.debug) System.out.println(this+" received ping"); - receivedPing = true; - if (data.equals(manualPing)) { - long nsPingDelay = System.nanoTime() - manualPingTimestamp; - double pingDelay = Math.round(nsPingDelay * 1.0 / 1E6) * 1.0 / 1E3; - System.out - .println("received ping from " + client.getUsername() + "; delay = " + pingDelay + " seconds."); - } - - return true; - } - - return serverInstance.parsePacket(this, inType, data); - } - - void doPing() { - sendData(InputType.PING, manualPing); - manualPingTimestamp = System.nanoTime(); - } - - void sendError(String message) { - if (Game.debug) - System.out.println("SERVER: sending error to " + client + ": \"" + message + "\""); - sendData(InputType.INVALID, message); - } - - void cachePacketTypes(List packetTypes) { - packetTypesToCache.addAll(packetTypes); - packetTypesToKeep.removeAll(packetTypes); - } - - void sendCachedPackets() { - packetTypesToCache.clear(); - - for (String packet : cachedPackets) { - InputType inType = InputType.values[Integer.parseInt(packet.substring(0, packet.indexOf(":")))]; - packet = packet.substring(packet.indexOf(":") + 1); - sendData(inType, packet); - } - - cachedPackets.clear(); - } - - protected void sendData(InputType inType, String data) { - if (packetTypesToCache.contains(inType)) - cachedPackets.add(inType.ordinal() + ":" + data); - else if (!packetTypesToKeep.contains(inType)) - super.sendData(inType, data); - } - - public void sendTileUpdate(Level level, int x, int y) { - sendTileUpdate(level.depth, x, y); - } - - public void sendTileUpdate(int depth, int x, int y) { - String data = Tile.getData(depth, x, y); - if (data.length() > 0) - sendData(InputType.TILE, data); - } - - public void sendEntityUpdate(Entity e, String updateString) { - if (updateString.length() > 0) { - // if (Game.debug && e instanceof Player) System.out.println("SERVER sending - // player update to " + client + ": " + e + "; data = " + updateString); - sendData(InputType.ENTITY, e.eid + ";" + updateString); - } // else - // if(Game.debug) System.out.println("SERVER: skipping entity update b/c no new - // fields: " + e); - } - - public void sendEntityAddition(Entity e) { - if (Game.debug && e instanceof Player) - System.out.println("SERVER: sending addition of player " + e + " to client through " + this); - if (Game.debug && e.eid == client.eid) - System.out.println("SERVER: sending addition of player to itself"); - String edata = Save.writeEntity(e, false); - if (edata.length() == 0) - System.out.println("entity not worth adding to client level: " + e + "; not sending to " + client); - else - sendData(InputType.ADD, edata); - } - - public void sendEntityRemoval(int eid, int levelDepth) { - sendData(InputType.REMOVE, String.valueOf(eid) + ";" + String.valueOf(levelDepth)); - } - - public void sendEntityRemoval(int eid) { // remove regardless of current level - sendData(InputType.REMOVE, String.valueOf(eid)); - } - - public void sendNotification(String note, int notetime) { - sendData(InputType.NOTIFY, notetime + ";" + note); - } - - public void sendPlayerHurt(int eid, int damage, Direction attackDir) { - sendData(InputType.HURT, eid + ";" + damage + ";" + attackDir.ordinal()); - } - - public void sendStopFishing(int eid) { - sendData(InputType.STOPFISHING, "" + eid); - } - - public void sendStaminaChange(int amt) { - sendData(InputType.STAMINA, amt + ""); - } - - public void updatePlayerActiveItem(Item heldItem) { - if (client.activeItem != null && !(client.activeItem instanceof PowerGloveItem)) - sendData(InputType.CHESTOUT, client.activeItem.getData()); - client.activeItem = heldItem; - - sendData(InputType.INTERACT, (client.activeItem == null ? "null" : client.activeItem.getData())); - } - - public void sendItems(String itemData) { - sendData(InputType.ADDITEMS, itemData); - } - - protected void respawnPlayer() { - client.remove(); // hopefully removes it from any level it might still be on - client = new RemotePlayer(false, client); - client.respawn(World.levels[World.lvlIdx(0)]); // get the spawn loc. of the client - sendData(InputType.PLAYER, client.getPlayerData()); // send spawn loc. - } - - @SuppressWarnings("resource") - private File getRemotePlayerFile() { - File[] clientFiles = serverInstance.getRemotePlayerFiles(); - - for (File file : clientFiles) { - String username = ""; - try { - BufferedReader br = new BufferedReader(new FileReader(file)); - try { - username = br.readLine().trim(); - } catch (IOException ex) { - System.err.println("failed to read line from file."); - ex.printStackTrace(); - } - } catch (FileNotFoundException ex) { - System.err.println("couldn't find remote player file: " + file); - ex.printStackTrace(); - } - - if (username.equals(client.getUsername())) { - /// this player has been here before. - if (Game.debug) - System.out.println("remote player file found; returning file " + file.getName()); - return file; - } - } - - return null; - } - - protected String getRemotePlayerFileData() { - File rpFile = getRemotePlayerFile(); - - String playerdata = ""; - if (rpFile != null && rpFile.exists()) { - try { - String content = Load.loadFromFile(rpFile.getPath(), false); - playerdata = content.substring(content.indexOf("\n") + 1); // cut off username - // assume the data version is dev6 if it isn't written (it isn't before dev7). - if (!Version.isValid(playerdata.substring(0, playerdata.indexOf("\n")))) - playerdata = "2.0.4-dev6\n" + playerdata; - } catch (IOException ex) { - System.err.println("failed to read remote player file: " + rpFile); - ex.printStackTrace(); - return ""; - } - } - - return playerdata; - } - - protected void writeClientSave(String playerdata) { - String filename; // this will hold the path to the file that will be saved to. - - File rpFile = getRemotePlayerFile(); - if (rpFile != null && rpFile.exists()) // check if this remote player already has a file. - filename = rpFile.getName(); - else { - File[] clientSaves = serverInstance.getRemotePlayerFiles(); - int numFiles = clientSaves.length; - filename = "RemotePlayer" + numFiles + Save.extension; - } - - String filedata = String.join("\n", client.getUsername(), playerdata); - - String filepath = serverInstance.getWorldPath() + "/" + filename; - try { - Save.writeToFile(filepath, filedata.split("\\n"), false); - } catch (IOException ex) { - System.err.println("problem writing remote player to file: " + filepath); - ex.printStackTrace(); - } - // the above will hopefully write the data to file. - } - - public void endConnection() { - pingTimer.stop(); - super.endConnection(); - - client.remove(); - - serverInstance.onThreadDisconnect(this); - } - - public String toString() { - return "ServerThread for " + (client == null ? "null" : client.getUsername()); - } -} + + private static final String autoPing = "ping"; + private static final String manualPing = "manual"; + + private static final int MISSED_PING_THRESHOLD = 5; + private static final int PING_INTERVAL = 1_000; // measured in milliseconds + + + private MinicraftServer serverInstance; + private RemotePlayer client; + + /// PING + + private Timer pingTimer; + private boolean receivedPing = true; // after first pause, it will act as if the ping was successful, since it didn't even send one in the first place and was just buying time for everything to get settled before pinging. + private int missedPings = 0; + + private long manualPingTimestamp; + + + private List packetTypesToKeep = new ArrayList<>(); + private List packetTypesToCache = new ArrayList<>(); + private List cachedPackets = new ArrayList<>(); + + private final boolean valid; + + MinicraftServerThread(Socket socket, MinicraftServer serverInstance) { + super("MinicraftServerThread", socket); + valid = true; + + this.serverInstance = serverInstance; + if (serverInstance.isFull()) { + sendError("Server at max capacity."); + super.endConnection(); + return; + } + + client = new RemotePlayer(null, false, socket.getInetAddress(), socket.getPort()); + + // username is set later + + packetTypesToKeep.addAll(InputType.tileUpdates); + packetTypesToKeep.addAll(InputType.entityUpdates); + + pingTimer = new Timer(PING_INTERVAL, e -> { + if (!isConnected()) { + pingTimer.stop(); + return; + } + + if (!receivedPing) { + missedPings++; + if (missedPings >= MISSED_PING_THRESHOLD) { + // disconnect from the client; they are taking too long to respond and probably don't exist at this point. + pingTimer.stop(); + sendError("Client ping too slow, server timed out"); + endConnection(); + } + } else { + missedPings = 0; + receivedPing = false; + } + + sendData(InputType.PING, autoPing); + }); + pingTimer.setRepeats(true); + pingTimer.setCoalesce(true); // don't try to make up for lost pings. + pingTimer.start(); + + start(); + } + + // this is to be a dummy thread. + MinicraftServerThread(RemotePlayer player, MinicraftServer server) { + super("MinicraftServerThread", null); + valid = false; + this.client = player; + this.serverInstance = server; + } + + public boolean isValid() { return valid; } + + public RemotePlayer getClient() { return client; } + + protected boolean parsePacket(InputType inType, String data) { + if (inType == InputType.PING) { + receivedPing = true; + if (data.equals(manualPing)) { + long nsPingDelay = System.nanoTime() - manualPingTimestamp; + double pingDelay = Math.round(nsPingDelay*1.0 / 1E6)*1.0 / 1E3; + System.out.println("Received ping from " + client.getUsername() + "; delay = " + pingDelay + " seconds."); + } + + return true; + } + + return serverInstance.parsePacket(this, inType, data); + } + + void doPing() { + sendData(InputType.PING, manualPing); + manualPingTimestamp = System.nanoTime(); + } + + void sendError(String message) { + if (Game.debug) System.out.println("SERVER: Sending error to " + client + ": \"" + message + "\""); + sendData(InputType.INVALID, message); + } + + void cachePacketTypes(List packetTypes) { + packetTypesToCache.addAll(packetTypes); + packetTypesToKeep.removeAll(packetTypes); + } + + void sendCachedPackets() { + packetTypesToCache.clear(); + + for (String packet: cachedPackets) { + InputType inType = InputType.values[Integer.parseInt(packet.substring(0, packet.indexOf(":")))]; + packet = packet.substring(packet.indexOf(":")+1); + sendData(inType, packet); + } + + cachedPackets.clear(); + } + + protected void sendData(InputType inType, String data) { + if (packetTypesToCache.contains(inType)) + cachedPackets.add(inType.ordinal()+ ":" +data); + else if (!packetTypesToKeep.contains(inType)) + super.sendData(inType, data); + } + + public void sendTileUpdate(Level level, int x, int y) { + sendTileUpdate(level.depth, x, y); + } + public void sendTileUpdate(int depth, int x, int y) { + String data = Tile.getData(depth, x, y); + if (data.length() > 0) + sendData(InputType.TILE, data); + } + + public void sendEntityUpdate(Entity e, String updateString) { + if (updateString.length() > 0) { + sendData(InputType.ENTITY, e.eid+ ";" +updateString); + } + } + + public void sendEntityAddition(Entity e) { + if (Game.debug && e instanceof Player) System.out.println("SERVER: Sending addition of player " + e + " to client through " + this); + if (Game.debug && e.eid == client.eid) System.out.println("SERVER: Sending addition of player to itself"); + String edata = Save.writeEntity(e, false); + if (edata.length() == 0) + System.out.println("Entity not worth adding to client level: " + e + "; not sending to " + client); + else + sendData(InputType.ADD, edata); + } + + public void sendEntityRemoval(int eid, int levelDepth) { + sendData(InputType.REMOVE, eid + ";" + levelDepth); + } + public void sendEntityRemoval(int eid) { // remove regardless of current level + sendData(InputType.REMOVE, String.valueOf(eid)); + } + + public void sendNotification(String note, int notetime) { + sendData(InputType.NOTIFY, notetime+ ";" +note); + } + + public void sendPlayerHurt(int eid, int damage, Direction attackDir) { + sendData(InputType.HURT, eid+ ";" +damage+ ";" +attackDir.ordinal()); + } + + public void sendStopFishing(int eid) { + sendData(InputType.STOPFISHING, "" + eid); + } + + public void sendStaminaChange(int amt) { + sendData(InputType.STAMINA, amt+ ""); + } + + public void updatePlayerActiveItem(Item heldItem) { + if (client.activeItem != null && !(client.activeItem instanceof PowerGloveItem)) + sendData(InputType.CHESTOUT, client.activeItem.getData()); + client.activeItem = heldItem; + + sendData(InputType.INTERACT, ( client.activeItem == null ? "null" : client.activeItem.getData() )); + } + + public void sendItems(String itemData) { + sendData(InputType.ADDITEMS, itemData); + } + + protected void respawnPlayer() { + client.remove(); // hopefully removes it from any level it might still be on + client = new RemotePlayer(false, client); + client.respawn(World.levels[World.lvlIdx(0)]); // get the spawn loc. of the client + sendData(InputType.PLAYER, client.getPlayerData()); // send spawn loc. + } + + private File getRemotePlayerFile() { + File[] clientFiles = serverInstance.getRemotePlayerFiles(); + + for (File file: clientFiles) { + String username = ""; + try { + BufferedReader br = new BufferedReader(new FileReader(file)); + try { + username = br.readLine().trim(); + } catch(IOException ex) { + System.err.println("Failed to read line from file."); + ex.printStackTrace(); + } + } catch(FileNotFoundException ex) { + System.err.println("Couldn't find remote player file: " + file); + ex.printStackTrace(); + } + + if (username.equals(client.getUsername())) { + /// this player has been here before. + if (Game.debug) System.out.println("Remote player file found; returning file " + file.getName()); + return file; + } + } + + return null; + } + + protected String getRemotePlayerFileData() { + File rpFile = getRemotePlayerFile(); + + String playerdata = ""; + if (rpFile != null && rpFile.exists()) { + try { + String content = Load.loadFromFile(rpFile.getPath(), false); + playerdata = content.substring(content.indexOf("\n")+1); // cut off username + // assume the data version is dev6 if it isn't written (it isn't before dev7). + if (!Version.isValid(playerdata.substring(0, playerdata.indexOf("\n")))) + playerdata = "2.0.4-dev6\n" +playerdata; + } catch(IOException ex) { + System.err.println("Failed to read remote player file: " + rpFile); + ex.printStackTrace(); + return ""; + } + } + + return playerdata; + } + + protected void writeClientSave(String playerdata) { + String filename; // this will hold the path to the file that will be saved to. + + File rpFile = getRemotePlayerFile(); + if (rpFile != null && rpFile.exists()) // check if this remote player already has a file. + filename = rpFile.getName(); + else { + File[] clientSaves = serverInstance.getRemotePlayerFiles(); + int numFiles = clientSaves.length; + filename = "RemotePlayer" + numFiles+Save.extension; + } + + String filedata = String.join("\n", client.getUsername(), playerdata); + + String filepath = serverInstance.getWorldPath() + "/" +filename; + try { + Save.writeToFile(filepath, filedata.split("\\n"), false); + } catch(IOException ex) { + System.err.println("Problem writing remote player to file: " + filepath); + ex.printStackTrace(); + } + // the above will hopefully write the data to file. + } + + public void endConnection() { + pingTimer.stop(); + super.endConnection(); + + client.remove(); + + serverInstance.onThreadDisconnect(this); + } + + public String toString() { + return "ServerThread for " + (client == null ? "null" : client.getUsername()); + } +} \ No newline at end of file diff --git a/src/minicraft/screen/Display.java b/src/minicraft/screen/Display.java index 951db6c1..8854888b 100644 --- a/src/minicraft/screen/Display.java +++ b/src/minicraft/screen/Display.java @@ -74,9 +74,7 @@ public void tick(InputHandler input) { boolean changedSelection = false; - if (menus.length > 1 && menus[selection].isSelectable()) { // if menu set is unselectable, it must have been - // intentional, so prevent the user from setting it - // back. + if (menus.length > 1 && menus[selection].isSelectable()) { // if menu set is unselectable, it must have been intentional, so prevent the user from setting it back. int prevSel = selection; String shift = menus[selection].getCurEntry() instanceof ArrayEntry ? "shift-" : ""; diff --git a/src/minicraft/screen/InventoryMenu.java b/src/minicraft/screen/InventoryMenu.java index 45860b2d..dadd009c 100644 --- a/src/minicraft/screen/InventoryMenu.java +++ b/src/minicraft/screen/InventoryMenu.java @@ -11,8 +11,8 @@ class InventoryMenu extends ItemListMenu { - private Inventory inv; - private Entity holder; + private final Inventory inv; + private final Entity holder; InventoryMenu(Entity holder, Inventory inv, String title) { super(InventoryMenu.getBuilder(), ItemEntry.useItems(inv.getItems()), title); diff --git a/src/minicraft/screen/MapData.java b/src/minicraft/screen/MapData.java index 00a4c1d5..db9bb38b 100644 --- a/src/minicraft/screen/MapData.java +++ b/src/minicraft/screen/MapData.java @@ -92,7 +92,7 @@ public enum MapData { WATER(Tiles.get("Water").id, Color.get(1, 26, 44, 137)), LAVA(Tiles.get("Lava").id, Color.RED), ROCK(Tiles.get("Rock").id, Color.get(1, 122, 122, 122)), - // SAND_ROCK(Tiles.get("Sand Rock").id, Color.get(1, 214, 214, 77)), + UP_ROCK(Tiles.get("Up Rock").id, Color.get(1, 147, 147, 147)), HARD_ROCK(Tiles.get("Hard Rock").id, Color.get(1, 127, 126, 107)), CACTUS(Tiles.get("Cactus").id, Color.get(1, 183, 183, 91)), diff --git a/src/minicraft/screen/PlayerInvDisplay.java b/src/minicraft/screen/PlayerInvDisplay.java index 44990766..786b7e17 100644 --- a/src/minicraft/screen/PlayerInvDisplay.java +++ b/src/minicraft/screen/PlayerInvDisplay.java @@ -2,29 +2,41 @@ import minicraft.core.Game; import minicraft.core.io.InputHandler; +import minicraft.core.io.Localization; import minicraft.entity.mob.Player; +import minicraft.gfx.Color; +import minicraft.gfx.Font; +import minicraft.gfx.Screen; public class PlayerInvDisplay extends Display { - private Player player; + private final Player player; - public PlayerInvDisplay(Player player) { - super(new InventoryMenu(player, player.getInventory(), "Inventory")); - this.player = player; - } + public PlayerInvDisplay(Player player) { + super(new InventoryMenu(player, player.getInventory(), "Inventory")); + this.player = player; + } + + @Override + public void tick(InputHandler input) { + super.tick(input); + + if(input.getKey("menu").clicked) { + Game.exitMenu(); + return; + } + + if(input.getKey("attack").clicked && menus[0].getNumOptions() > 0) { + player.activeItem = player.getInventory().remove(menus[0].getSelection()); + Game.exitMenu(); + } + } - @Override - public void tick(InputHandler input) { - super.tick(input); + @Override + public void render(Screen screen) { + super.render(screen); - if (input.getKey("menu").clicked) { - Game.exitMenu(); - return; - } - - if (input.getKey("attack").clicked && menus[0].getNumOptions() > 0) { - player.activeItem = player.getInventory().remove(menus[0].getSelection()); - Game.exitMenu(); - } - } + String text = "(" + Game.input.getMapping("SEARCHER-BAR") + ") " + Localization.getLocalized("to search."); + Font.draw(text, screen, Screen.w / 4 - 75 - text.length(), Screen.h / 2 - 46, Color.WHITE); + } } diff --git a/src/minicraft/screen/TexturePackDisplay.java b/src/minicraft/screen/TexturePackDisplay.java index 5c234990..1bd68b34 100644 --- a/src/minicraft/screen/TexturePackDisplay.java +++ b/src/minicraft/screen/TexturePackDisplay.java @@ -123,7 +123,7 @@ public void tick(InputHandler input) { // In case the name is too big ... private static String shortNameIfLong(String name) { - return name.length() > 24 ? name.substring(0, 16) + "..." : name; + return name.length() > 32 ? name.substring(0, 16) + "..." : name; } @Override diff --git a/src/minicraft/screen/TitleDisplay.java b/src/minicraft/screen/TitleDisplay.java index 19f1b388..63165a46 100644 --- a/src/minicraft/screen/TitleDisplay.java +++ b/src/minicraft/screen/TitleDisplay.java @@ -38,10 +38,10 @@ public TitleDisplay() { new SelectEntry("Singleplayer", () -> { if (WorldSelectDisplay.getWorldNames().size() > 0) Game.setMenu(new Display(true, - new Menu.Builder(false, 2, RelPos.CENTER, - new SelectEntry("Load World", () -> Game.setMenu(new WorldSelectDisplay())), - new SelectEntry("New World", () -> Game.setMenu(new WorldGenDisplay()))) - .createMenu())); + new Menu.Builder(false, 2, RelPos.CENTER, + new SelectEntry("Load World", () -> Game.setMenu(new WorldSelectDisplay())), + new SelectEntry("New World", () -> Game.setMenu(new WorldGenDisplay()))) + .createMenu())); else Game.setMenu(new WorldGenDisplay()); }), @@ -49,17 +49,17 @@ public TitleDisplay() { new SelectEntry("Options", () -> Game.setMenu(new OptionsDisplay())), new SelectEntry("Credits", () -> Game.setMenu(new BookDisplay(BookData.credits))), displayFactory("Help", - new SelectEntry("Instructions", () -> Game.setMenu(new BookDisplay(BookData.instructions))), - new BlankEntry(), - // new SelectEntry("Storyline Guide", () -> Game.setMenu(new - // BookDisplay(BookData.storylineGuide))), - new SelectEntry("Tutorial", () -> Game.setMenu(new TutorialDisplay())), new BlankEntry(), - new SelectEntry("About", () -> Game.setMenu(new BookDisplay(BookData.about))), new BlankEntry(), - new BlankEntry(), new LinkEntry(Color.BLUE, "Minicraft discord", "https://discord.me/minicraft") + new SelectEntry("Instructions", () -> Game.setMenu(new BookDisplay(BookData.instructions))), + new BlankEntry(), + // new SelectEntry("Storyline Guide", () -> Game.setMenu(new + // BookDisplay(BookData.storylineGuide))), + new SelectEntry("Tutorial", () -> Game.setMenu(new TutorialDisplay())), new BlankEntry(), + new SelectEntry("About", () -> Game.setMenu(new BookDisplay(BookData.about))), new BlankEntry(), + new BlankEntry(), new LinkEntry(Color.BLUE, "Minicraft discord", "https://discord.me/minicraft") ), new SelectEntry("Exit", Game::quit)) - .setPositioning(new Point(Screen.w / 2, Screen.h * 3 / 5), RelPos.CENTER).createMenu()); + .setPositioning(new Point(Screen.w / 2, Screen.h * 3 / 5), RelPos.CENTER).createMenu()); } @Override @@ -70,10 +70,9 @@ public void init(Display parent) { if (random.nextInt(2) == 0) { Sound.Intro.play(); - } - if (random.nextInt(2) == 1) { + } else { + Sound.Intro2.play(); - } /// This is useful to just ensure that everything is really reset as it should @@ -148,9 +147,8 @@ public void init(Display parent) { } @NotNull - private static SelectEntry displayFactory(String entryText, ListEntry... entries) { - return new SelectEntry(entryText, - () -> Game.setMenu(new Display(true, new Menu.Builder(false, 2, RelPos.CENTER, entries).createMenu()))); + private static SelectEntry displayFactory(String entryText, ListEntry...entries) { + return new SelectEntry(entryText, () -> Game.setMenu(new Display(true, new Menu.Builder(false, 2, RelPos.CENTER, entries).createMenu()))); } @Override @@ -209,14 +207,13 @@ public void render(Screen screen) { boolean isOrange = splashes[rand].contains("Orange"); boolean isYellow = splashes[rand].contains("Yellow"); - /// This isn't as complicated as it looks. It just gets a color based off of - /// count, which oscilates between 0 and 25. + /// This isn't as complicated as it looks. It just gets a color based off of count, which oscilates between 0 and 25. int bcol = 5 - count / 5; // this number ends up being between 1 and 5, inclusive. - int splashColor = isblue ? Color.BLUE - : isRed ? Color.RED - : isGreen ? Color.GREEN - : isOrange ? Color.ORANGE - : isYellow ? Color.YELLOW : Color.get(1, bcol * 51, bcol * 51, bcol * 25); + int splashColor = isblue ? Color.BLUE : + isRed ? Color.RED : + isGreen ? Color.GREEN : + isOrange ? Color.ORANGE : + isYellow ? Color.YELLOW : Color.get(1, bcol * 51, bcol * 51, bcol * 25); Font.drawCentered(splashes[rand], screen, 100, splashColor); @@ -238,99 +235,224 @@ public void render(Screen screen) { Font.draw("Mod by TheBigEye", screen, 300, 280, Color.GRAY); } - private static final String[] splashes = { "Happy birthday Minicraft!", "Happy XMAS!", "Happy birthday Eye :)", - "Happy birthday Zaq :)", "Thanks A.L.I.C.E!", - - // "Bye ben :(", - - // Also play - "Also play Minicraft Plus!", "Also play InfinityTale!", "Also play Minicraft Deluxe!", - "Also play Alecraft!", "Also play Hackcraft!", "Also play MiniCrate!", "Also play MiniCraft Mob Overload!", - "Also play Minitale!, oh right :(", - - "Playing " + Game.BUILD + ", nice!", "Based in Minicraft+, nice!", "Updates always!, nice?", - - // Now with... - "Now with better fishing!", "Now with better Weapons!", "Now with better tools!", "Now with better chests!", - "Now with better dungeons!", "Now with better sounds!", "Air Wizard now with phases!", - - "Only on PlayMinicraft.com!", "Playminicraft.com is the bomb!", "@MinicraftPlus on Twitter", - "MinicraftPlus on Youtube", "Join the Forums!", "The Wiki is weak! Help it!", "Great little community!", - - "Notch is Awesome!", "Dillyg10 is cool as Ice!", "Shylor is the man!", "Chris J is great with portals!", - "AntVenom loves cows! Honest!", "The eye and Cake rain!", "ASCII", "32.872 lines of code!", - - "Nobody should read this! #404", "You should read Antidious Venomi!", "Oh Hi Mark", "Use the force!", - "Keep calm!", "Get him, Steve!", "Forty-Two!", "A hostile paradise", - - // kill - "Kill Creeper, get Gunpowder!", "Kill Cow, get Beef!", "Kill Zombie, get Cloth!", "Kill Slime, get Slime!", - "Kill Slime, get Problems!", "Kill Skeleton, get Bones!", "Kill Skeleton, get Arrows!", - "Kill Sheep, get Wool!", "Kill Goat, get Leather!", "Kill Pig, get Porkchop!", - "Kill Chicken, get Feathers!", "Kill Guiman, get more Feathers!", - - // Mineral levels - "Gold > Iron", "Gem > Gold", - - "Test == InDev!", "Story? yes!", "Mod on phase B-eta", - - "Axes: good against plants!", "Picks: good against rocks!", "Shovels: good against dirt!", - "Swords: good against mobs!", - - // What's that? - "Infinite terrain? What's that?", "Ceilings? What's is that?", "Redstone? What's that?", - "Minecarts? What are those?", "Windows? I prefer Doors!", "2.5D FTW!", "Grab your friends!", - "Sky?, better Aether!", - - // Not Included - "Null not included", "Humans not included", "Herobine not included?", "Mouse not included!", - "No spiders included!", "No Endermen included!", "3rd dimension not included!", "Orange box not included!", - "Alpha version not included!", "Cthulhu sold separately!", "Skins not included!", - - // Included - "Villagers included!", "Creepers included!", "Skeletons included!", "Knights included!", "Snakes included!", - "Cows included!", "Sheep included!", "Chickens included!", "Goats included!", "Pigs included!", - "Cthulhu included?", "Enchantments Now Included!", "Multiplayer Now Included!", "Carrots Now Included!", - "Potatos Now Included!", "Boats Now Included!", "Maps Now Included!", "Books included!", - "Sad music included!", "Big eye included!", - // "Nether Now Included?", - - "Shhh!,secret dimension!", "A nice cup of coffee!", - - // Worlds - "Bigger Worlds!", "World types!", "World themes!", "Mushroom Biome!", "Desert Biome!", "Forest Biome!", - "Snow Biome!", "Better sky", "Slow world gen :(", - - // Ideas - "Sugarcane is a Idea!", "Milk is an idea!", "Cakes is an idea!", "Coffee is another idea!", - - "Texture packs!", - - "Creeper, aw man", "So we back in the mine,", "pickaxe swinging from side to side", "In search of Gems!", - "Life itself suspended by a thread", "saying ay-oh, that creeper's KO'd!", - - "Gimmie a bucket!", "Farming with water!", "Press \"R\"!", "Get the High-Score!", "I see a dreamer!", - "Potions ftw!", "Beds ftw!", - - "Defeat the Air Wizard!", "Defeat the Eye queen!", "Defeat the Keeper!", "Defeat me...", - - "Conquer the Dungeon!", "One down, one to go...", "Loom + Wool = String!", "String + Wood = Rod!", - "Sand + Gunpowder = TNT!", - - "Try Eyenglish!", - - "Sleep at Night!", "Farm at Day!", - - "Explanation Mark!", "!sdrawkcab si sihT", "This is forwards!", "Why is this blue?", - "Green is a nice color!", "Red is my favorite color!", "Hmmm Orange!", "Yellow = Happy!", - // "Y U NO BOAT!?", - "Made with 10000% Vitamin Z!", "Too much DP!", "Punch the Moon!", "This is String qq!", "Why?", - "You are null!", "hello down there!", "That guy is such a sly fox!", "Hola senor!", "Sonic Boom!", - "Hakuna Matata!", "One truth prevails!", "Awesome!", "Sweet!", "Great!", "Cool!", "Radical!", - "011011000110111101101100!", "001100010011000000110001!", "011010000110110101101101?", "...zzz...", - - // Tributes - "Rick May, 1940 - 2020", - - "Something cool is coming ;)", }; + private static final String[] splashes = { + "Happy birthday Minicraft!", + "Happy XMAS!", + "Happy birthday Eye :)", + "Happy birthday Zaq :)", + "Thanks A.L.I.C.E!", + + // "Bye ben :(", + + // Also play + "Also play Minicraft Plus!", + "Also play InfinityTale!", + "Also play Minicraft Deluxe!", + "Also play Alecraft!", + "Also play Hackcraft!", + "Also play MiniCrate!", + "Also play MiniCraft Mob Overload!", + "Also play Minitale!, oh right :(", + + "Playing " + Game.BUILD + ", nice!", + "Based in Minicraft+, nice!", + "Updates always!, nice?", + + // Now with... + "Now with better fishing!", + "Now with better Weapons!", + "Now with better tools!", + "Now with better chests!", + "Now with better dungeons!", + "Now with better sounds!", + "Air Wizard now with phases!", + + "Only on PlayMinicraft.com!", + "Playminicraft.com is the bomb!", + "@MinicraftPlus on Twitter", + "MinicraftPlus on Youtube", + "Join the Forums!", + "The Wiki is weak! Help it!", + "Great little community!", + + "Notch is Awesome!", + "Dillyg10 is cool as Ice!", + "Shylor is the man!", + "Chris J is great with portals!", + "AntVenom loves cows! Honest!", + "The eye and Cake rain!", + "ASCII", + "32.872 lines of code!", + + "Nobody should read this! #404", + "You should read Antidious Venomi!", + "Oh Hi Mark", + "Use the force!", + "Keep calm!", + "Get him, Steve!", + "Forty-Two!", + "A hostile paradise", + + // kill + "Kill Creeper, get Gunpowder!", + "Kill Cow, get Beef!", + "Kill Zombie, get Cloth!", + "Kill Slime, get Slime!", + "Kill Slime, get Problems!", + "Kill Skeleton, get Bones!", + "Kill Skeleton, get Arrows!", + "Kill Sheep, get Wool!", + "Kill Goat, get Leather!", + "Kill Pig, get Porkchop!", + "Kill Chicken, get Feathers!", + "Kill Guiman, get more Feathers!", + + // Mineral levels + "Gold > Iron", + "Gem > Gold", + + "Test == InDev!", + "Story? yes!", + "Mod on phase B-eta", + + "Axes: good against plants!", + "Picks: good against rocks!", + "Shovels: good against dirt!", + "Swords: good against mobs!", + + // What's that? + "Infinite terrain? What's that?", + "Ceilings? What's is that?", + "Redstone? What's that?", + "Minecarts? What are those?", + "Windows? I prefer Doors!", + "2.5D FTW!", + "Grab your friends!", + "Sky?, better Aether!", + + // Not Included + "Null not included", + "Humans not included", + "Herobine not included?", + "Mouse not included!", + "No spiders included!", + "No Endermen included!", + "3rd dimension not included!", + "Orange box not included!", + "Alpha version not included!", + "Cthulhu sold separately!", + "Skins not included!", + + // Included + "Villagers included!", + "Creepers included!", + "Skeletons included!", + "Knights included!", + "Snakes included!", + "Cows included!", + "Sheep included!", + "Chickens included!", + "Goats included!", + "Pigs included!", + "Cthulhu included?", + "Enchantments Now Included!", + "Multiplayer Now Included!", + "Carrots Now Included!", + "Potatos Now Included!", + "Boats Now Included!", + "Maps Now Included!", + "Books included!", + "Sad music included!", + "Big eye included!", + // "Nether Now Included?", + + "Shhh!,secret dimension!", + "A nice cup of coffee!", + + // Worlds + "Bigger Worlds!", + "World types!", + "World themes!", + "Mushroom Biome!", + "Desert Biome!", + "Forest Biome!", + "Snow Biome!", + "Better sky", + "Slow world gen :(", + + // Ideas + "Sugarcane is a Idea!", + "Milk is an idea!", + "Cakes is an idea!", + "Coffee is another idea!", + + "Texture packs!", + + "Creeper, aw man", + "So we back in the mine,", + "pickaxe swinging from side to side", + "In search of Gems!", + "Life itself suspended by a thread", + "saying ay-oh, that creeper's KO'd!", + + "Gimmie a bucket!", + "Farming with water!", + "Press \"R\"!", + "Get the High-Score!", + "I see a dreamer!", + "Potions ftw!", + "Beds ftw!", + + "Defeat the Air Wizard!", + "Defeat the Eye queen!", + "Defeat the Keeper!", + "Defeat me...", + + "Conquer the Dungeon!", + "One down, one to go...", + "Loom + Wool = String!", + "String + Wood = Rod!", + "Sand + Gunpowder = TNT!", + + "Try Eyenglish!", + + "Sleep at Night!", + "Farm at Day!", + + "Explanation Mark!", + "!sdrawkcab si sihT", + "This is forwards!", + "Why is this blue?", + "Green is a nice color!", + "Red is my favorite color!", + "Hmmm Orange!", + "Yellow = Happy!", + // "Y U NO BOAT!?", + "Made with 10000% Vitamin Z!", + "Too much DP!", + "Punch the Moon!", + "This is String qq!", + "Why?", + "You are null!", + "hello down there!", + "That guy is such a sly fox!", + "Hola senor!", + "Sonic Boom!", + "Hakuna Matata!", + "One truth prevails!", + "Awesome!", + "Sweet!", + "Great!", + "Cool!", + "Radical!", + "011011000110111101101100!", + "001100010011000000110001!", + "011010000110110101101101?", + "...zzz...", + + // Tributes + "Rick May, 1940 - 2020", + + "Something cool is coming ;)", + }; } \ No newline at end of file diff --git a/src/minicraft/screen/entry/ListEntry.java b/src/minicraft/screen/entry/ListEntry.java index 9c14081c..37695b8f 100644 --- a/src/minicraft/screen/entry/ListEntry.java +++ b/src/minicraft/screen/entry/ListEntry.java @@ -35,8 +35,7 @@ public void render(Screen screen, int x, int y, boolean isSelected, String conta String string = toString().toLowerCase(Locale.ENGLISH); contain = contain.toLowerCase(Locale.ENGLISH); - Font.drawColor(string.replaceAll(contain, Color.toStringCode(containColor) + contain + Color.WHITE_CODE), - screen, x, y); + Font.drawColor(string.replace(contain, Color.toStringCode(containColor) + contain + Color.WHITE_CODE), screen, x, y); } /**