units(){
+ return getBy(ContentType.unit);
+ }
+
+ /**
+ * Registers sync IDs for all types of sync entities.
+ * Do not register units here!
+ */
+ private void registerTypes(){
+ TypeTrait.registerType(Player.class, Player::new);
+ TypeTrait.registerType(Fire.class, Fire::new);
+ TypeTrait.registerType(Puddle.class, Puddle::new);
+ }
+}
diff --git a/core/src/io/anuke/mindustry/core/Control.java b/core/src/io/anuke/mindustry/core/Control.java
index c319cc7e0e..3b87d16091 100644
--- a/core/src/io/anuke/mindustry/core/Control.java
+++ b/core/src/io/anuke/mindustry/core/Control.java
@@ -1,410 +1,329 @@
package io.anuke.mindustry.core;
-import com.badlogic.gdx.Gdx;
-import com.badlogic.gdx.Input.Buttons;
-import com.badlogic.gdx.graphics.Color;
+import io.anuke.arc.*;
+import io.anuke.arc.graphics.Color;
+import io.anuke.arc.graphics.GL20;
+import io.anuke.arc.graphics.g2d.Draw;
+import io.anuke.arc.graphics.g2d.TextureAtlas;
+import io.anuke.arc.input.KeyCode;
+import io.anuke.arc.scene.ui.Dialog;
+import io.anuke.arc.scene.ui.TextField;
+import io.anuke.arc.util.*;
import io.anuke.mindustry.core.GameState.State;
-import io.anuke.mindustry.entities.Player;
+import io.anuke.mindustry.entities.Effects;
+import io.anuke.mindustry.entities.type.Player;
+import io.anuke.mindustry.game.*;
import io.anuke.mindustry.game.EventType.*;
-import io.anuke.mindustry.game.Tutorial;
-import io.anuke.mindustry.game.UpgradeInventory;
-import io.anuke.mindustry.graphics.Fx;
-import io.anuke.mindustry.input.AndroidInput;
-import io.anuke.mindustry.input.DefaultKeybinds;
-import io.anuke.mindustry.input.DesktopInput;
-import io.anuke.mindustry.input.InputHandler;
-import io.anuke.mindustry.io.Saves;
+import io.anuke.mindustry.gen.Call;
+import io.anuke.mindustry.input.*;
+import io.anuke.mindustry.maps.Map;
import io.anuke.mindustry.net.Net;
-import io.anuke.mindustry.resource.Item;
-import io.anuke.mindustry.resource.Weapon;
-import io.anuke.mindustry.world.Map;
-import io.anuke.ucore.UCore;
-import io.anuke.ucore.core.*;
-import io.anuke.ucore.core.Inputs.DeviceType;
-import io.anuke.ucore.entities.Entities;
-import io.anuke.ucore.modules.Module;
-import io.anuke.ucore.scene.ui.layout.Unit;
-import io.anuke.ucore.util.Atlas;
-import io.anuke.ucore.util.InputProxy;
-import io.anuke.ucore.util.Mathf;
+import io.anuke.mindustry.type.*;
+import io.anuke.mindustry.ui.dialogs.FloatingDialog;
+import io.anuke.mindustry.world.Tile;
+import java.io.IOException;
+import java.nio.IntBuffer;
+
+import static io.anuke.arc.Core.scene;
import static io.anuke.mindustry.Vars.*;
-/**Control module.
+/**
+ * Control module.
* Handles all input, saving, keybinds and keybinds.
- * Should not handle any game-critical state.
- * This class is not created in the headless server.*/
-public class Control extends Module{
- private UpgradeInventory upgrades = new UpgradeInventory();
- private Tutorial tutorial = new Tutorial();
- private boolean hiscore = false;
+ * Should not handle any logic-critical state.
+ * This class is not created in the headless server.
+ */
+public class Control implements ApplicationListener{
+ public final Saves saves;
- private boolean wasPaused = false;
+ private Interval timer = new Interval(2);
+ private boolean hiscore = false;
+ private boolean wasPaused = false;
+ private InputHandler input;
- private Saves saves;
+ public Control(){
+ IntBuffer buf = BufferUtils.newIntBuffer(1);
+ Core.gl.glGetIntegerv(GL20.GL_MAX_TEXTURE_SIZE, buf);
+ int maxSize = buf.get(0);
- private float respawntime;
- private InputHandler input;
+ saves = new Saves();
+ data = new GlobalData();
- private InputProxy proxy;
- private float controlx, controly;
- private boolean controlling;
- private Throwable error;
+ Core.input.setCatch(KeyCode.BACK, true);
- public Control(){
- saves = new Saves();
+ Effects.setShakeFalloff(10000f);
- Inputs.useControllers(!gwt);
+ content.initialize(Content::init);
+ Core.atlas = new TextureAtlas(maxSize < 2048 ? "sprites/sprites_fallback.atlas" : "sprites/sprites.atlas");
+ Draw.scl = 1f / Core.atlas.find("scale_marker").getWidth();
+ content.initialize(Content::load, true);
- Gdx.input.setCatchBackKey(true);
+ data.load();
- if(mobile){
- input = new AndroidInput();
- }else{
- input = new DesktopInput();
- }
+ Core.settings.setAppName(appName);
+ Core.settings.defaults(
+ "ip", "localhost",
+ "color-0", Color.rgba8888(playerColors[8]),
+ "color-1", Color.rgba8888(playerColors[11]),
+ "color-2", Color.rgba8888(playerColors[13]),
+ "color-3", Color.rgba8888(playerColors[9]),
+ "name", "",
+ "lastBuild", 0
+ );
- proxy = new InputProxy(Gdx.input){
- @Override
- public int getY() {
- return controlling ? (int)controly : input.getY();
+ createPlayer();
+
+ saves.load();
+
+ Events.on(StateChangeEvent.class, event -> {
+ if((event.from == State.playing && event.to == State.menu) || (event.from == State.menu && event.to != State.menu)){
+ Time.runTask(5f, Platform.instance::updateRPC);
}
+ });
- @Override
- public int getX() {
- return controlling ? (int)controlx : input.getX();
- }
+ Events.on(PlayEvent.class, event -> {
+ player.setTeam(defaultTeam);
+ player.setDead(true);
+ player.add();
- @Override
- public int getY(int pointer) {
- return pointer == 0 ? getY() : super.getY(pointer);
- }
-
- @Override
- public int getX(int pointer) {
- return pointer == 0 ? getX() : super.getX(pointer);
- }
- };
-
- Inputs.addProcessor(input);
-
- Effects.setShakeFalloff(10000f);
-
- Core.atlas = new Atlas("sprites.atlas");
-
- for(Item item : Item.getAllItems()){
- item.init();
- }
-
- Sounds.load("shoot.mp3", "place.mp3", "explosion.mp3", "enemyshoot.mp3",
- "corexplode.mp3", "break.mp3", "spawn.mp3", "flame.mp3", "die.mp3",
- "respawn.mp3", "purchase.mp3", "flame2.mp3", "bigshot.mp3", "laser.mp3", "lasershot.mp3",
- "ping.mp3", "tesla.mp3", "waveend.mp3", "railgun.mp3", "blast.mp3", "bang2.mp3");
-
- Sounds.setFalloff(9000f);
-
- Musics.load("1.mp3", "2.mp3", "3.mp3", "4.mp3", "5.mp3", "6.mp3");
-
- DefaultKeybinds.load();
-
- for(int i = 0; i < saveSlots; i ++){
- Settings.defaults("save-" + i + "-autosave", !gwt);
- Settings.defaults("save-" + i + "-name", "untitled");
- Settings.defaults("save-" + i + "-data", "empty");
- }
-
- Settings.defaultList(
- "ip", "localhost",
- "port", port+"",
- "name", mobile || gwt ? "player" : UCore.getProperty("user.name"),
- "servers", "",
- "color", Color.rgba8888(playerColors[8]),
- "lastVersion", "3.2",
- "lastBuild", 0
- );
-
- KeyBinds.load();
-
- for(Map map : world.maps().list()){
- Settings.defaults("hiscore" + map.name, 0);
- }
-
- player = new Player();
- player.name = Settings.getString("name");
- player.isAndroid = mobile;
- player.color.set(Settings.getInt("color"));
- player.isLocal = true;
-
- saves.load();
-
- Events.on(StateChangeEvent.class, (from, to) -> {
- if((from == State.playing && to == State.menu) || (from == State.menu && to != State.menu)){
- Timers.runTask(5f, Platform.instance::updateRPC);
- }
- });
-
- Events.on(PlayEvent.class, () -> {
- renderer.clearTiles();
-
- player.set(world.getSpawnX(), world.getSpawnY());
-
- Core.camera.position.set(player.x, player.y, 0);
-
- ui.hudfrag.updateItems();
-
- state.set(State.playing);
- });
-
- Events.on(ResetEvent.class, () -> {
- upgrades.reset();
- player.weaponLeft = player.weaponRight = Weapon.blaster;
-
- player.add();
- player.heal();
-
- respawntime = -1;
- hiscore = false;
-
- ui.hudfrag.updateItems();
- ui.hudfrag.updateWeapons();
- ui.hudfrag.fadeRespawn(false);
- });
-
- Events.on(WaveEvent.class, () -> {
- Sounds.play("spawn");
-
- int last = Settings.getInt("hiscore" + world.getMap().name, 0);
-
- if(state.wave > last && !state.mode.infiniteResources && !state.mode.disableWaveTimer){
- Settings.putInt("hiscore" + world.getMap().name, state.wave);
- Settings.save();
- hiscore = true;
- }
-
- Platform.instance.updateRPC();
- });
-
- Events.on(GameOverEvent.class, () -> {
- Effects.shake(5, 6, Core.camera.position.x, Core.camera.position.y);
- Sounds.play("corexplode");
- for(int i = 0; i < 16; i ++){
- Timers.run(i*2, ()-> Effects.effect(Fx.explosion, world.getCore().worldx()+Mathf.range(40), world.getCore().worldy()+Mathf.range(40)));
- }
- Effects.effect(Fx.coreexplosion, world.getCore().worldx(), world.getCore().worldy());
-
- ui.restart.show();
-
- Timers.runTask(30f, () -> state.set(State.menu));
- });
- }
-
- //FIXME figure out what's causing this problem in the first place
- public void triggerInputUpdate(){
- Gdx.input = proxy;
- }
-
- public void setError(Throwable error){
- this.error = error;
- }
-
- public UpgradeInventory upgrades() {
- return upgrades;
- }
-
- public Saves getSaves(){
- return saves;
- }
-
- public boolean showCursor(){
- return controlling;
- }
-
- public InputHandler input(){
- return input;
- }
-
- public void playMap(Map map){
- ui.loadfrag.show();
- saves.resetSave();
-
- Timers.runTask(10, () -> {
- logic.reset();
- world.loadMap(map);
- logic.play();
- });
-
- Timers.runTask(18, () -> ui.loadfrag.hide());
- }
-
- public boolean isHighScore(){
- return hiscore;
- }
-
- public float getRespawnTime(){
- return respawntime;
- }
-
- public void setRespawnTime(float respawntime){
- this.respawntime = respawntime;
- }
-
- public Tutorial tutorial(){
- return tutorial;
- }
-
- private void checkOldUser(){
- boolean hasPlayed = false;
-
- for(Map map : world.maps().getAllMaps()){
- if(Settings.getInt("hiscore" + map.name) != 0){
- hasPlayed = true;
- break;
- }
- }
-
- if(hasPlayed && Settings.getString("lastVersion").equals("3.2")){
- Timers.runTask(1f, () -> ui.showInfo("$text.changes"));
- Settings.putString("lastVersion", "3.3");
- Settings.save();
- }
- }
-
- @Override
- public void dispose(){
- Platform.instance.onGameExit();
- Net.dispose();
- }
-
- @Override
- public void pause(){
- wasPaused = state.is(State.paused);
- if(state.is(State.playing)) state.set(State.paused);
- }
-
- @Override
- public void resume(){
- if(state.is(State.paused) && !wasPaused){
state.set(State.playing);
- }
- }
+ });
- @Override
- public void init(){
- Timers.run(1f, Musics::shuffleAll);
+ Events.on(WorldLoadEvent.class, event -> {
+ Core.app.post(() -> Core.app.post(() -> {
+ if(Net.active() && player.getClosestCore() != null){
+ //set to closest core since that's where the player will probably respawn; prevents camera jumps
+ Core.camera.position.set(player.getClosestCore());
+ }else{
+ //locally, set to player position since respawning occurs immediately
+ Core.camera.position.set(player);
+ }
+ }));
+ });
- Entities.initPhysics();
- Entities.collisions().setCollider(tilesize, world::solid);
+ Events.on(ResetEvent.class, event -> {
+ player.reset();
- Platform.instance.updateRPC();
+ hiscore = false;
- checkOldUser();
- }
+ saves.resetSave();
+ });
- @Override
- public void update(){
-
- if(error != null){
- throw new RuntimeException(error);
- }
-
- Gdx.input = proxy;
-
- if(Inputs.keyTap("console")){
- console = !console;
- }
-
- if(KeyBinds.getSection("default").device.type == DeviceType.controller){
- if(Inputs.keyTap("select")){
- Inputs.getProcessor().touchDown(Gdx.input.getX(), Gdx.input.getY(), 0, Buttons.LEFT);
+ Events.on(WaveEvent.class, event -> {
+ if(world.getMap().getHightScore() < state.wave){
+ hiscore = true;
+ world.getMap().setHighScore(state.wave);
}
+ });
- if(Inputs.keyRelease("select")){
- Inputs.getProcessor().touchUp(Gdx.input.getX(), Gdx.input.getY(), 0, Buttons.LEFT);
+ Events.on(GameOverEvent.class, event -> {
+ state.stats.wavesLasted = state.wave;
+ Effects.shake(5, 6, Core.camera.position.x, Core.camera.position.y);
+ //the restart dialog can show info for any number of scenarios
+ Call.onGameOver(event.winner);
+ if(state.rules.zone != null){
+ //remove zone save on game over
+ if(saves.getZoneSlot() != null){
+ saves.getZoneSlot().delete();
+ }
}
+ });
- float xa = Inputs.getAxis("cursor_x");
- float ya = Inputs.getAxis("cursor_y");
-
- if(Math.abs(xa) > controllerMin || Math.abs(ya) > controllerMin) {
- float scl = Settings.getInt("sensitivity")/100f * Unit.dp.scl(1f);
- controlx += xa*baseControllerSpeed*scl;
- controly -= ya*baseControllerSpeed*scl;
- controlling = true;
-
- Gdx.input.setCursorCatched(true);
-
- Inputs.getProcessor().touchDragged(Gdx.input.getX(), Gdx.input.getY(), 0);
+ //autohost for pvp maps
+ Events.on(WorldLoadEvent.class, event -> {
+ if(state.rules.pvp && !Net.active()){
+ try{
+ Net.host(port);
+ player.isAdmin = true;
+ }catch(IOException e){
+ ui.showError(Core.bundle.format("server.error", Strings.parseException(e, true)));
+ Core.app.post(() -> state.set(State.menu));
+ }
}
+ });
- controlx = Mathf.clamp(controlx, 0, Gdx.graphics.getWidth());
- controly = Mathf.clamp(controly, 0, Gdx.graphics.getHeight());
+ Events.on(UnlockEvent.class, e -> ui.hudfrag.showUnlock(e.content));
- if(Gdx.input.getDeltaX() > 1 || Gdx.input.getDeltaY() > 1) {
- controlling = false;
- Gdx.input.setCursorCatched(false);
- }
+ Events.on(BlockBuildEndEvent.class, e -> {
+ if(e.team == player.getTeam()){
+ if(e.breaking){
+ state.stats.buildingsDeconstructed++;
+ }else{
+ state.stats.buildingsBuilt++;
+ }
+ }
+ });
+
+ Events.on(BlockDestroyEvent.class, e -> {
+ if(e.tile.getTeam() == player.getTeam()){
+ state.stats.buildingsDestroyed++;
+ }
+ });
+
+ Events.on(UnitDestroyEvent.class, e -> {
+ if(e.unit.getTeam() != player.getTeam()){
+ state.stats.enemyUnitsDestroyed++;
+ }
+ });
+
+ Events.on(ZoneRequireCompleteEvent.class, e -> {
+ ui.hudfrag.showToast(Core.bundle.format("zone.requirement.complete", state.wave, e.zone.localizedName));
+ });
+
+ Events.on(ZoneConfigureCompleteEvent.class, e -> {
+ ui.hudfrag.showToast(Core.bundle.format("zone.config.complete", e.zone.configureWave));
+ });
+ }
+
+ void createPlayer(){
+ player = new Player();
+ player.name = Core.settings.getString("name");
+ player.color.set(Core.settings.getInt("color-0"));
+ player.isLocal = true;
+ player.isMobile = mobile;
+
+ if(mobile){
+ input = new MobileInput();
}else{
- controlling = false;
- Gdx.input.setCursorCatched(false);
+ input = new DesktopInput();
}
- if(!controlling){
- controlx = Gdx.input.getX();
- controly = Gdx.input.getY();
+ if(!state.is(State.menu)){
+ player.add();
}
+ Core.input.addProcessor(input);
+ }
+
+ public InputHandler input(){
+ return input;
+ }
+
+ public void playMap(Map map, Rules rules){
+ ui.loadAnd(() -> {
+ logic.reset();
+ world.loadMap(map);
+ state.rules = rules;
+ logic.play();
+ });
+ }
+
+ public void playZone(Zone zone){
+ ui.loadAnd(() -> {
+ logic.reset();
+ world.loadGenerator(zone.generator);
+ zone.rules.accept(state.rules);
+ state.rules.zone = zone;
+ for(Tile core : state.teams.get(defaultTeam).cores){
+ for(ItemStack stack : zone.getStartingItems()){
+ core.entity.items.add(stack.item, stack.amount);
+ }
+ }
+ state.set(State.playing);
+ control.saves.zoneSave();
+ logic.play();
+ });
+ }
+
+ public boolean isHighScore(){
+ return hiscore;
+ }
+
+ @Override
+ public void dispose(){
+ content.dispose();
+ Net.dispose();
+ ui.editor.dispose();
+ }
+
+ @Override
+ public void pause(){
+ wasPaused = state.is(State.paused);
+ if(state.is(State.playing)) state.set(State.paused);
+ }
+
+ @Override
+ public void resume(){
+ if(state.is(State.paused) && !wasPaused){
+ state.set(State.playing);
+ }
+ }
+
+ @Override
+ public void init(){
+ Platform.instance.updateRPC();
+
+ if(!Core.settings.getBool("4.0-warning-2", false)){
+
+ Time.run(5f, () -> {
+ FloatingDialog dialog = new FloatingDialog("VERY IMPORTANT");
+ dialog.buttons.addButton("$ok", () -> {
+ dialog.hide();
+ Core.settings.put("4.0-warning-2", true);
+ Core.settings.save();
+ }).size(100f, 60f);
+ dialog.cont.add("Reminder: The alpha version you are about to play is very unstable, and is [accent]not representative of the final v4 release.[]\n\n " +
+ "\nThere is currently[scarlet] no sound implemented[]; this is intentional.\n" +
+ "All current art and UI is unfinished, and will be changed before release. " +
+ "\n\n[accent]Saves may be corrupted without warning between updates.").wrap().width(400f);
+ dialog.show();
+ });
+ }
+ }
+
+ @Override
+ public void update(){
saves.update();
- if(state.inventory.isUpdated() && (Timers.get("updateItems", 8) || state.is(State.paused))){
- ui.hudfrag.updateItems();
- state.inventory.setUpdated(false);
- }
+ input.updateController();
- if(!state.is(State.menu)){
- input.update();
+ //autosave global data if it's modified
+ data.checkSave();
- if(Inputs.keyTap("pause") && !ui.restart.isShown() && (state.is(State.paused) || state.is(State.playing))){
+ if(!state.is(State.menu)){
+ input.update();
+
+ if(world.isZone()){
+ for(Tile tile : state.teams.get(player.getTeam()).cores){
+ for(Item item : content.items()){
+ if(tile.entity.items.has(item)){
+ data.unlockContent(item);
+ }
+ }
+ }
+ }
+
+ //auto-update rpc every 5 seconds
+ if(timer.get(0, 60 * 5)){
+ Platform.instance.updateRPC();
+ }
+
+ if(Core.input.keyTap(Binding.pause) && !ui.restart.isShown() && (state.is(State.paused) || state.is(State.playing))){
state.set(state.is(State.playing) ? State.paused : State.playing);
- }
+ }
- if(Inputs.keyTap("menu")){
- if(state.is(State.paused)){
- ui.paused.hide();
- state.set(State.playing);
- }else if (!ui.restart.isShown()){
- if(ui.chatfrag.chatOpen()) {
- ui.chatfrag.hide();
- }else{
- ui.paused.show();
- state.set(State.paused);
- }
- }
- }
+ if(Core.input.keyTap(Binding.menu) && !ui.restart.isShown()){
+ if(ui.chatfrag.chatOpen()){
+ ui.chatfrag.hide();
+ }else if(!ui.paused.isShown() && !scene.hasDialog()){
+ ui.paused.show();
+ state.set(State.paused);
+ }
+ }
- if(!state.is(State.paused) || Net.active()){
- Entities.update(effectGroup);
+ if(!mobile && Core.input.keyTap(Binding.screenshot) && !(scene.getKeyboardFocus() instanceof TextField) && !ui.chatfrag.chatOpen()){
+ renderer.takeMapScreenshot();
+ }
- if(respawntime > 0){
+ }else{
+ if(!state.isPaused()){
+ Time.update();
+ }
- respawntime -= Timers.delta();
-
- if(respawntime <= 0){
- player.set(world.getSpawnX(), world.getSpawnY());
- player.heal();
- player.add();
- Effects.sound("respawn");
- ui.hudfrag.fadeRespawn(false);
- }
- }
-
- if(tutorial.active()){
- tutorial.update();
- }
- }
- }else{
- if(!state.is(State.paused) || Net.active()){
- Timers.update();
- }
- }
- }
+ if(!scene.hasDialog() && !(scene.root.getChildren().peek() instanceof Dialog) && Core.input.keyTap(KeyCode.BACK)){
+ Platform.instance.hide();
+ }
+ }
+ }
}
diff --git a/core/src/io/anuke/mindustry/core/GameState.java b/core/src/io/anuke/mindustry/core/GameState.java
index 20acc304e3..1e755de6c6 100644
--- a/core/src/io/anuke/mindustry/core/GameState.java
+++ b/core/src/io/anuke/mindustry/core/GameState.java
@@ -1,40 +1,63 @@
package io.anuke.mindustry.core;
-import io.anuke.mindustry.game.Difficulty;
+import io.anuke.arc.Events;
+import io.anuke.mindustry.entities.type.BaseUnit;
+import io.anuke.mindustry.entities.type.base.BaseDrone;
import io.anuke.mindustry.game.EventType.StateChangeEvent;
-import io.anuke.mindustry.game.GameMode;
-import io.anuke.mindustry.game.Inventory;
-import io.anuke.ucore.core.Events;
+import io.anuke.mindustry.game.*;
+import io.anuke.mindustry.net.Net;
+
+import static io.anuke.mindustry.Vars.unitGroups;
+import static io.anuke.mindustry.Vars.waveTeam;
public class GameState{
- private State state = State.menu;
+ /** Current wave number, can be anything in non-wave modes. */
+ public int wave = 1;
+ /** Wave countdown in ticks. */
+ public float wavetime;
+ /** Whether the game is in game over state. */
+ public boolean gameOver = false, launched = false;
+ /** The current game rules. */
+ public Rules rules = new Rules();
+ /** Statistics for this save/game. Displayed after game over. */
+ public Stats stats = new Stats();
+ /** Team data. Gets reset every new game. */
+ public Teams teams = new Teams();
+ /** Number of enemies in the game; only used clientside in servers. */
+ public int enemies;
+ /** Current game state. */
+ private State state = State.menu;
- public final Inventory inventory = new Inventory();
+ public int enemies(){
+ return Net.client() ? enemies : unitGroups[waveTeam.ordinal()].count(b -> !(b instanceof BaseDrone));
+ }
- public int wave = 1;
- public int lastUpdated = -1;
- public float wavetime;
- public float extrawavetime;
- public int enemies = 0;
- public boolean gameOver = false;
- public GameMode mode = GameMode.waves;
- public Difficulty difficulty = Difficulty.normal;
- public boolean friendlyFire;
-
- public void set(State astate){
- Events.fire(StateChangeEvent.class, state, astate);
- state = astate;
- }
-
- public boolean is(State astate){
- return state == astate;
- }
+ public BaseUnit boss(){
+ return unitGroups[waveTeam.ordinal()].find(BaseUnit::isBoss);
+ }
- public State getState(){
- return state;
- }
-
- public enum State{
- paused, playing, menu
- }
+ public void set(State astate){
+ Events.fire(new StateChangeEvent(state, astate));
+ state = astate;
+ }
+
+ public boolean isEditor(){
+ return rules.editor;
+ }
+
+ public boolean isPaused(){
+ return (is(State.paused) && !Net.active()) || (gameOver && !Net.active());
+ }
+
+ public boolean is(State astate){
+ return state == astate;
+ }
+
+ public State getState(){
+ return state;
+ }
+
+ public enum State{
+ paused, playing, menu
+ }
}
diff --git a/core/src/io/anuke/mindustry/core/Logic.java b/core/src/io/anuke/mindustry/core/Logic.java
index cb4f484c73..b4a2fd9b76 100644
--- a/core/src/io/anuke/mindustry/core/Logic.java
+++ b/core/src/io/anuke/mindustry/core/Logic.java
@@ -1,109 +1,167 @@
package io.anuke.mindustry.core;
-import com.badlogic.gdx.utils.Array;
+import io.anuke.annotations.Annotations.Loc;
+import io.anuke.annotations.Annotations.Remote;
+import io.anuke.arc.ApplicationListener;
+import io.anuke.arc.Events;
+import io.anuke.arc.collection.ObjectSet.ObjectSetIterator;
+import io.anuke.arc.util.Time;
+import io.anuke.mindustry.content.*;
import io.anuke.mindustry.core.GameState.State;
-import io.anuke.mindustry.entities.enemies.Enemy;
-import io.anuke.mindustry.game.EnemySpawn;
-import io.anuke.mindustry.game.EventType.GameOverEvent;
-import io.anuke.mindustry.game.EventType.PlayEvent;
-import io.anuke.mindustry.game.EventType.ResetEvent;
-import io.anuke.mindustry.game.EventType.WaveEvent;
-import io.anuke.mindustry.game.SpawnPoint;
-import io.anuke.mindustry.game.WaveCreator;
-import io.anuke.mindustry.graphics.Fx;
+import io.anuke.mindustry.entities.*;
+import io.anuke.mindustry.entities.type.Player;
+import io.anuke.mindustry.entities.type.TileEntity;
+import io.anuke.mindustry.game.EventType.*;
+import io.anuke.mindustry.game.*;
+import io.anuke.mindustry.game.Teams.TeamData;
+import io.anuke.mindustry.gen.BrokenBlock;
import io.anuke.mindustry.net.Net;
-import io.anuke.mindustry.net.NetEvents;
+import io.anuke.mindustry.type.Item;
+import io.anuke.mindustry.world.Block;
import io.anuke.mindustry.world.Tile;
-import io.anuke.mindustry.world.blocks.ProductionBlocks;
-import io.anuke.ucore.core.Effects;
-import io.anuke.ucore.core.Events;
-import io.anuke.ucore.core.Timers;
-import io.anuke.ucore.entities.Entities;
-import io.anuke.ucore.modules.Module;
-import io.anuke.ucore.util.Mathf;
+import io.anuke.mindustry.world.blocks.BuildBlock;
+import io.anuke.mindustry.world.blocks.BuildBlock.BuildEntity;
import static io.anuke.mindustry.Vars.*;
-/**Logic module.
+/**
+ * Logic module.
* Handles all logic for entities and waves.
* Handles game state events.
* Does not store any game state itself.
- *
+ *
* This class should not call any outside methods to change state of modules, but instead fire events.
*/
-public class Logic extends Module {
- private final Array spawns = WaveCreator.getSpawns();
+public class Logic implements ApplicationListener{
- @Override
- public void init(){
- Entities.initPhysics();
- Entities.collisions().setCollider(tilesize, world::solid);
+ public Logic(){
+ Events.on(WaveEvent.class, event -> {
+ if(world.isZone()){
+ world.getZone().updateWave(state.wave);
+ }
+ for (Player p : playerGroup.all()) {
+ p.respawns = state.rules.respawns;
+ }
+ });
+
+ Events.on(BlockDestroyEvent.class, event -> {
+ //blocks that get broken are appended to the team's broken block queue
+ Tile tile = event.tile;
+ Block block = tile.block();
+ if(block instanceof BuildBlock){
+ BuildEntity entity = tile.entity();
+
+ //update block to reflect the fact that something was being constructed
+ if(entity.cblock != null && entity.cblock.synthetic()){
+ block = entity.cblock;
+ }else{
+ //otherwise this was a deconstruction that was interrupted, don't want to rebuild that
+ return;
+ }
+ }
+
+ TeamData data = state.teams.get(tile.getTeam());
+ data.brokenBlocks.addFirst(BrokenBlock.get(tile.x, tile.y, tile.rotation(), block.id));
+ });
+ }
+
+ /** Handles the event of content being used by either the player or some block. */
+ public void handleContent(UnlockableContent content){
+ if(!headless){
+ data.unlockContent(content);
+ }
}
public void play(){
- state.wavetime = wavespace * state.difficulty.timeScaling * 2;
+ state.set(State.playing);
+ state.wavetime = state.rules.waveSpacing * 2; //grace period of 2x wave time before game starts
+ Events.fire(new PlayEvent());
- if(state.mode.infiniteResources){
- state.inventory.fill();
+ //add starting items
+ if(!world.isZone()){
+ for(Team team : Team.all){
+ if(state.teams.isActive(team)){
+ for(Tile core : state.teams.get(team).cores){
+ core.entity.items.add(Items.copper, 200);
+ }
+ }
+ }
}
-
- Events.fire(PlayEvent.class);
}
public void reset(){
state.wave = 1;
- state.extrawavetime = maxwavespace * state.difficulty.maxTimeScaling;
- state.wavetime = wavespace * state.difficulty.timeScaling;
- state.enemies = 0;
- state.lastUpdated = -1;
- state.gameOver = false;
- state.inventory.clearItems();
+ state.wavetime = state.rules.waveSpacing;
+ state.gameOver = state.launched = false;
+ state.teams = new Teams();
+ state.rules = new Rules();
+ state.stats = new Stats();
- Timers.clear();
+ Time.clear();
Entities.clear();
+ TileEntity.sleepingEntities = 0;
- Events.fire(ResetEvent.class);
+ Events.fire(new ResetEvent());
}
public void runWave(){
+ world.spawner.spawnEnemies();
+ state.wave++;
+ state.wavetime = world.isZone() && world.getZone().isBossWave(state.wave) ? state.rules.waveSpacing * state.rules.bossWaveMultiplier :
+ world.isZone() && world.getZone().isLaunchWave(state.wave) ? state.rules.waveSpacing * state.rules.launchWaveMultiplier : state.rules.waveSpacing;
- if(state.lastUpdated < state.wave + 1){
- world.pathfinder().resetPaths();
- state.lastUpdated = state.wave + 1;
- }
+ Events.fire(new WaveEvent());
+ }
- for(EnemySpawn spawn : spawns){
- Array spawns = world.getSpawns();
+ private void checkGameOver(){
+ if(!state.rules.attackMode && state.teams.get(defaultTeam).cores.size == 0 && !state.gameOver){
+ state.gameOver = true;
+ Events.fire(new GameOverEvent(waveTeam));
+ }else if(state.rules.attackMode){
+ Team alive = null;
- for(int lane = 0; lane < spawns.size; lane ++){
- int fl = lane;
- Tile tile = spawns.get(lane).start;
- int spawnamount = spawn.evaluate(state.wave, lane);
-
- for(int i = 0; i < spawnamount; i ++){
- float range = 12f;
-
- Timers.runTask(i*5f, () -> {
-
- Enemy enemy = new Enemy(spawn.type);
- enemy.set(tile.worldx() + Mathf.range(range), tile.worldy() + Mathf.range(range));
- enemy.lane = fl;
- enemy.tier = spawn.tier(state.wave, fl);
- enemy.add();
-
- Effects.effect(Fx.spawn, enemy);
-
- state.enemies ++;
- });
+ for(Team team : Team.all){
+ if(state.teams.get(team).cores.size > 0){
+ if(alive != null){
+ return;
+ }
+ alive = team;
}
}
+
+ if(alive != null && !state.gameOver){
+ state.gameOver = true;
+ Events.fire(new GameOverEvent(alive));
+ }
+ }
+ }
+
+ @Remote(called = Loc.both)
+ public static void launchZone(){
+ if(!headless){
+ ui.hudfrag.showLaunch();
}
- state.wave ++;
- state.wavetime = wavespace * state.difficulty.timeScaling;
- state.extrawavetime = maxwavespace * state.difficulty.maxTimeScaling;
+ for(Tile tile : new ObjectSetIterator<>(state.teams.get(defaultTeam).cores)){
+ Effects.effect(Fx.launch, tile);
+ }
- Events.fire(WaveEvent.class);
+ Time.runTask(30f, () -> {
+ for(Tile tile : new ObjectSetIterator<>(state.teams.get(defaultTeam).cores)){
+ for(Item item : content.items()){
+ data.addItem(item, tile.entity.items.get(item));
+ }
+ world.removeBlock(tile);
+ }
+ state.launched = true;
+ });
+ }
+
+ @Remote(called = Loc.both)
+ public static void onGameOver(Team winner){
+ state.stats.wavesLasted = state.wave;
+ ui.restart.show(winner);
+ netClient.setQuiet();
}
@Override
@@ -111,50 +169,64 @@ public class Logic extends Module {
if(!state.is(State.menu)){
- if(control != null) control.triggerInputUpdate();
+ if(!state.isPaused()){
+ Time.update();
- if(!state.is(State.paused) || Net.active()){
- Timers.update();
- }
-
- if(!Net.client())
- world.pathfinder().update();
-
- if(world.getCore() != null && world.getCore().block() != ProductionBlocks.core && !state.gameOver){
- state.gameOver = true;
- if(Net.server()) NetEvents.handleGameOver();
- Events.fire(GameOverEvent.class);
- }
-
- if(!state.is(State.paused) || Net.active()){
-
- if(!state.mode.disableWaveTimer){
-
- if(state.enemies <= 0){
- if(!world.getMap().name.equals("tutorial")) state.wavetime -= Timers.delta();
-
- if(state.lastUpdated < state.wave + 1 && state.wavetime < aheadPathfinding){ //start updating beforehand
- world.pathfinder().resetPaths();
- state.lastUpdated = state.wave + 1;
- }
- }else if(!world.getMap().name.equals("tutorial")){
- state.extrawavetime -= Timers.delta();
+ if(state.rules.waves && state.rules.waveTimer && !state.gameOver){
+ if(!state.rules.waitForWaveToEnd || unitGroups[waveTeam.ordinal()].size() == 0){
+ state.wavetime = Math.max(state.wavetime - Time.delta(), 0);
}
}
- if(!Net.client() && (state.wavetime <= 0 || state.extrawavetime <= 0)){
+ if(!Net.client() && state.wavetime <= 0 && state.rules.waves){
runWave();
}
- Entities.update(Entities.defaultGroup());
- Entities.update(bulletGroup);
- Entities.update(enemyGroup);
- Entities.update(tileGroup);
- Entities.update(shieldGroup);
+ if(!headless){
+ Entities.update(effectGroup);
+ Entities.update(groundEffectGroup);
+ }
+
+ if(!state.isEditor()){
+ for(EntityGroup group : unitGroups){
+ Entities.update(group);
+ }
+
+ Entities.update(puddleGroup);
+ Entities.update(shieldGroup);
+ Entities.update(bulletGroup);
+ Entities.update(tileGroup);
+ Entities.update(fireGroup);
+ }else{
+ for(EntityGroup> group : unitGroups){
+ group.updateEvents();
+ collisions.updatePhysics(group);
+ }
+ }
+
+
Entities.update(playerGroup);
- Entities.collideGroups(bulletGroup, enemyGroup);
- Entities.collideGroups(bulletGroup, playerGroup);
+ //effect group only contains item transfers in the headless version, update it!
+ if(headless){
+ Entities.update(effectGroup);
+ }
+
+ if(!state.isEditor()){
+
+ for(EntityGroup group : unitGroups){
+ if(group.isEmpty()) continue;
+ collisions.collideGroups(bulletGroup, group);
+ }
+
+ collisions.collideGroups(bulletGroup, playerGroup);
+ }
+
+ world.pathfinder.update();
+ }
+
+ if(!Net.client() && !world.isInvalidMap() && !state.isEditor()){
+ checkGameOver();
}
}
}
diff --git a/core/src/io/anuke/mindustry/core/NetClient.java b/core/src/io/anuke/mindustry/core/NetClient.java
index 85fc3b3ebc..05ea541609 100644
--- a/core/src/io/anuke/mindustry/core/NetClient.java
+++ b/core/src/io/anuke/mindustry/core/NetClient.java
@@ -1,82 +1,89 @@
package io.anuke.mindustry.core;
-import com.badlogic.gdx.graphics.Color;
-import com.badlogic.gdx.utils.IntMap;
-import com.badlogic.gdx.utils.IntSet;
-import com.badlogic.gdx.utils.TimeUtils;
+import io.anuke.annotations.Annotations.*;
+import io.anuke.arc.ApplicationListener;
+import io.anuke.arc.Core;
+import io.anuke.arc.collection.IntSet;
+import io.anuke.arc.graphics.Color;
+import io.anuke.arc.math.RandomXS128;
+import io.anuke.arc.util.*;
+import io.anuke.arc.util.io.ReusableByteInStream;
+import io.anuke.arc.util.serialization.Base64Coder;
import io.anuke.mindustry.Vars;
import io.anuke.mindustry.core.GameState.State;
-import io.anuke.mindustry.entities.Bullet;
-import io.anuke.mindustry.entities.BulletType;
-import io.anuke.mindustry.entities.Player;
-import io.anuke.mindustry.entities.SyncEntity;
-import io.anuke.mindustry.entities.enemies.Enemy;
-import io.anuke.mindustry.net.Net;
+import io.anuke.mindustry.entities.Entities;
+import io.anuke.mindustry.entities.EntityGroup;
+import io.anuke.mindustry.entities.traits.BuilderTrait.BuildRequest;
+import io.anuke.mindustry.entities.traits.SyncTrait;
+import io.anuke.mindustry.entities.traits.TypeTrait;
+import io.anuke.mindustry.entities.type.Player;
+import io.anuke.mindustry.entities.type.Unit;
+import io.anuke.mindustry.game.Version;
+import io.anuke.mindustry.gen.Call;
+import io.anuke.mindustry.gen.RemoteReadClient;
+import io.anuke.mindustry.net.Administration.TraceInfo;
+import io.anuke.mindustry.net.*;
import io.anuke.mindustry.net.Net.SendMode;
-import io.anuke.mindustry.net.NetworkIO;
import io.anuke.mindustry.net.Packets.*;
-import io.anuke.mindustry.resource.Item;
-import io.anuke.mindustry.resource.Upgrade;
-import io.anuke.mindustry.resource.UpgradeRecipes;
-import io.anuke.mindustry.resource.Weapon;
-import io.anuke.mindustry.world.Block;
-import io.anuke.mindustry.world.Map;
-import io.anuke.mindustry.world.Placement;
import io.anuke.mindustry.world.Tile;
-import io.anuke.mindustry.world.blocks.ProductionBlocks;
-import io.anuke.ucore.core.Effects;
-import io.anuke.ucore.core.Timers;
-import io.anuke.ucore.entities.BaseBulletType;
-import io.anuke.ucore.entities.Entities;
-import io.anuke.ucore.entities.Entity;
-import io.anuke.ucore.entities.EntityGroup;
-import io.anuke.ucore.modules.Module;
-import io.anuke.ucore.util.Log;
-import io.anuke.ucore.util.Timer;
+import io.anuke.mindustry.world.modules.ItemModule;
-import java.nio.ByteBuffer;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.util.zip.InflaterInputStream;
import static io.anuke.mindustry.Vars.*;
-public class NetClient extends Module {
- private final static float dataTimeout = 60*18; //18 seconds timeout
+public class NetClient implements ApplicationListener{
+ private final static float dataTimeout = 60 * 18;
private final static float playerSyncTime = 2;
- private final static int maxRequests = 50;
+ public final static float viewScale = 2f;
- private Timer timer = new Timer(5);
+ private Interval timer = new Interval(5);
+ /** Whether the client is currently connecting. */
private boolean connecting = false;
- private boolean kicked = false;
- private IntSet recieved = new IntSet();
- private IntMap recent = new IntMap<>();
- private int requests = 0;
- private float timeoutTime = 0f; //data timeout counter
+ /** If true, no message will be shown on disconnect. */
+ private boolean quiet = false;
+ /** Counter for data timeout. */
+ private float timeoutTime = 0f;
+ /** Last sent client snapshot ID. */
+ private int lastSent;
+
+ /** List of entities that were removed, and need not be added while syncing. */
+ private IntSet removed = new IntSet();
+ /** Byte stream for reading in snapshots. */
+ private ReusableByteInStream byteStream = new ReusableByteInStream();
+ private DataInputStream dataStream = new DataInputStream(byteStream);
public NetClient(){
Net.handleClient(Connect.class, packet -> {
+ Log.info("Connecting to server: {0}", packet.addressTCP);
+
player.isAdmin = false;
- Net.setClientLoaded(false);
- recieved.clear();
- recent.clear();
- timeoutTime = 0f;
- connecting = true;
- kicked = false;
+ reset();
- ui.chatfrag.clearMessages();
ui.loadfrag.hide();
- ui.loadfrag.show("$text.connecting.data");
+ ui.loadfrag.show("$connecting.data");
- Entities.clear();
+ ui.loadfrag.setButton(() -> {
+ ui.loadfrag.hide();
+ connecting = false;
+ quiet = true;
+ Net.disconnect();
+ });
ConnectPacket c = new ConnectPacket();
c.name = player.name;
- c.android = mobile;
+ c.mobile = mobile;
+ c.versionType = Version.type;
c.color = Color.rgba8888(player.color);
+ c.usid = getUsid(packet.addressTCP);
c.uuid = Platform.instance.getUUID();
if(c.uuid == null){
- ui.showError("$text.invalidid");
+ ui.showError("$invalidid");
ui.loadfrag.hide();
disconnectQuietly();
return;
@@ -86,238 +93,207 @@ public class NetClient extends Module {
});
Net.handleClient(Disconnect.class, packet -> {
- if (kicked) return;
-
- Timers.runTask(3f, ui.loadfrag::hide);
-
state.set(State.menu);
-
- ui.showError("$text.disconnect");
connecting = false;
-
+ logic.reset();
Platform.instance.updateRPC();
+
+ if(quiet) return;
+
+ Time.runTask(3f, ui.loadfrag::hide);
+
+ ui.showError("$disconnect");
});
- Net.handleClient(WorldData.class, data -> {
+ Net.handleClient(WorldStream.class, data -> {
Log.info("Recieved world data: {0} bytes.", data.stream.available());
- NetworkIO.loadWorld(data.stream);
- player.set(world.getSpawnX(), world.getSpawnY());
+ NetworkIO.loadWorld(new InflaterInputStream(data.stream));
finishConnecting();
});
- Net.handleClient(CustomMapPacket.class, packet -> {
- Log.info("Recieved custom map: {0} bytes.", packet.stream.available());
-
- //custom map is always sent before world data
- Map map = NetworkIO.loadMap(packet.stream);
-
- world.maps().setNetworkMap(map);
-
- MapAckPacket ack = new MapAckPacket();
- Net.send(ack, SendMode.tcp);
+ Net.handleClient(InvokePacket.class, packet -> {
+ packet.writeBuffer.position(0);
+ RemoteReadClient.readPacket(packet.writeBuffer, packet.type);
});
+ }
- Net.handleClient(SyncPacket.class, packet -> {
- if (connecting) return;
- int players = 0;
- int enemies = 0;
+ //called on all clients
+ @Remote(called = Loc.server, targets = Loc.server, variants = Variant.both)
+ public static void sendMessage(String message, String sender, Player playersender){
+ if(Vars.ui != null){
+ Vars.ui.chatfrag.addMessage(message, sender);
+ }
- ByteBuffer data = ByteBuffer.wrap(packet.data);
- long time = data.getLong();
+ if(playersender != null){
+ playersender.lastText = message;
+ playersender.textFadeTime = 1f;
+ }
+ }
- byte groupid = data.get();
+ //equivalent to above method but there's no sender and no console log
+ @Remote(called = Loc.server, targets = Loc.server)
+ public static void sendMessage(String message){
+ if(Vars.ui != null){
+ Vars.ui.chatfrag.addMessage(message, null);
+ }
+ }
- EntityGroup> group = Entities.getGroup(groupid);
+ //called when a server recieves a chat message from a player
+ @Remote(called = Loc.server, targets = Loc.client)
+ public static void sendChatMessage(Player player, String message){
+ if(message.length() > maxTextLength){
+ throw new ValidateException(player, "Player has sent a message above the text limit.");
+ }
- while (data.position() < data.capacity()) {
- int id = data.getInt();
+ //server console logging
+ Log.info("&y{0}: &lb{1}", player.name, message);
- SyncEntity entity = (SyncEntity) group.getByID(id);
+ //invoke event for all clients but also locally
+ //this is required so other clients get the correct name even if they don't know who's sending it yet
+ Call.sendMessage(message, colorizeName(player.id, player.name), player);
+ }
- if(entity instanceof Player) players ++;
- if(entity instanceof Enemy) enemies ++;
+ private static String colorizeName(int id, String name){
+ Player player = playerGroup.getByID(id);
+ if(name == null || player == null) return null;
+ return "[#" + player.color.toString().toUpperCase() + "]" + name;
+ }
- if (entity == null || id == player.id) {
- if (id != player.id && requests < maxRequests) {
- EntityRequestPacket req = new EntityRequestPacket();
- req.id = id;
- req.group = groupid;
- Net.send(req, SendMode.udp);
- requests ++;
+ @Remote(variants = Variant.one)
+ public static void onTraceInfo(Player player, TraceInfo info){
+ if(player != null){
+ ui.traces.show(player, info);
+ }
+ }
+
+ @Remote(variants = Variant.one, priority = PacketPriority.high)
+ public static void onKick(KickReason reason){
+ netClient.disconnectQuietly();
+ state.set(State.menu);
+
+ if(!reason.quiet){
+ if(reason.extraText() != null){
+ ui.showText(reason.toString(), reason.extraText());
+ }else{
+ ui.showText("$disconnect", reason.toString());
+ }
+ }
+ ui.loadfrag.hide();
+ }
+
+ @Remote(variants = Variant.both)
+ public static void onInfoMessage(String message){
+ ui.showText("", message);
+ }
+
+ @Remote(variants = Variant.both)
+ public static void onWorldDataBegin(){
+ Entities.clear();
+ netClient.removed.clear();
+ logic.reset();
+
+ ui.chatfrag.clearMessages();
+ Net.setClientLoaded(false);
+
+ ui.loadfrag.show("$connecting.data");
+
+ ui.loadfrag.setButton(() -> {
+ ui.loadfrag.hide();
+ netClient.connecting = false;
+ netClient.quiet = true;
+ Net.disconnect();
+ });
+ }
+
+ @Remote(variants = Variant.one)
+ public static void onPositionSet(float x, float y){
+ player.x = x;
+ player.y = y;
+ }
+
+ @Remote
+ public static void onPlayerDisconnect(int playerid){
+ playerGroup.removeByID(playerid);
+ }
+
+ @Remote(variants = Variant.one, priority = PacketPriority.low, unreliable = true)
+ public static void onEntitySnapshot(byte groupID, short amount, short dataLen, byte[] data){
+ try{
+ netClient.byteStream.setBytes(Net.decompressSnapshot(data, dataLen));
+ DataInputStream input = netClient.dataStream;
+
+ EntityGroup group = Entities.getGroup(groupID);
+
+ //go through each entity
+ for(int j = 0; j < amount; j++){
+ int id = input.readInt();
+ byte typeID = input.readByte();
+
+ SyncTrait entity = group == null ? null : (SyncTrait)group.getByID(id);
+ boolean add = false, created = false;
+
+ if(entity == null && id == player.id){
+ entity = player;
+ add = true;
+ }
+
+ //entity must not be added yet, so create it
+ if(entity == null){
+ entity = (SyncTrait)TypeTrait.getTypeByID(typeID).get(); //create entity from supplier
+ entity.resetID(id);
+ if(!netClient.isEntityUsed(entity.getID())){
+ add = true;
}
- data.position(data.position() + SyncEntity.getWriteSize((Class extends SyncEntity>) group.getType()));
- } else {
- entity.read(data, time);
+ created = true;
+ }
+
+ //read the entity
+ entity.read(input);
+
+ if(created && entity.getInterpolator() != null && entity.getInterpolator().target != null){
+ //set initial starting position
+ entity.setNet(entity.getInterpolator().target.x, entity.getInterpolator().target.y);
+ if(entity instanceof Unit && entity.getInterpolator().targets.length > 0){
+ ((Unit)entity).rotation = entity.getInterpolator().targets[0];
+ }
+ }
+
+ if(add){
+ entity.add();
+ netClient.addRemovedEntity(entity.getID());
+ }
+ }
+ }catch(IOException e){
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Remote(variants = Variant.one, priority = PacketPriority.low, unreliable = true)
+ public static void onStateSnapshot(float waveTime, int wave, int enemies, short coreDataLen, byte[] coreData){
+ try{
+ state.wavetime = waveTime;
+ state.wave = wave;
+ state.enemies = enemies;
+
+ netClient.byteStream.setBytes(Net.decompressSnapshot(coreData, coreDataLen));
+ DataInputStream input = netClient.dataStream;
+
+ byte cores = input.readByte();
+ for(int i = 0; i < cores; i++){
+ int pos = input.readInt();
+ Tile tile = world.tile(pos);
+
+ if(tile != null && tile.entity != null){
+ tile.entity.items.read(input);
+ }else{
+ new ItemModule().read(input);
}
}
- if(debugNet){
- clientDebug.setSyncDebug(players, enemies);
- }
- });
-
- Net.handleClient(StateSyncPacket.class, packet -> {
-
- System.arraycopy(packet.items, 0, state.inventory.getItems(), 0, packet.items.length);
-
- state.enemies = packet.enemies;
- state.wavetime = packet.countdown;
- state.wave = packet.wave;
-
- ui.hudfrag.updateItems();
- });
-
- Net.handleClient(BlockLogRequestPacket.class, packet -> {
- currentEditLogs = packet.editlogs;
- });
-
- Net.handleClient(PlacePacket.class, (packet) -> {
- Placement.placeBlock(packet.x, packet.y, Block.getByID(packet.block), packet.rotation, true, Timers.get("placeblocksound", 10));
-
- if(packet.playerid == player.id){
- Tile tile = world.tile(packet.x, packet.y);
- if(tile != null) Block.getByID(packet.block).placed(tile);
- }
- });
-
- Net.handleClient(BreakPacket.class, (packet) ->
- Placement.breakBlock(packet.x, packet.y, true, Timers.get("breakblocksound", 10)));
-
- Net.handleClient(EntitySpawnPacket.class, packet -> {
- EntityGroup group = packet.group;
-
- //duplicates.
- if (group.getByID(packet.entity.id) != null ||
- recieved.contains(packet.entity.id)) return;
-
- recieved.add(packet.entity.id);
- recent.put(packet.entity.id, packet.entity);
-
- packet.entity.add();
-
- Log.info("Recieved entity {0}", packet.entity.id);
- });
-
- Net.handleClient(EnemyDeathPacket.class, packet -> {
- Enemy enemy = enemyGroup.getByID(packet.id);
- if (enemy != null){
- enemy.type.onDeath(enemy, true);
- }else if(recent.get(packet.id) != null){
- recent.get(packet.id).remove();
- }else{
- Log.err("Got remove for null entity! {0}", packet.id);
- }
- recieved.add(packet.id);
- });
-
- Net.handleClient(BulletPacket.class, packet -> {
- //TODO shoot effects for enemies, clientside as well as serverside
- BulletType type = (BulletType) BaseBulletType.getByID(packet.type);
- Entity owner = enemyGroup.getByID(packet.owner);
- new Bullet(type, owner, packet.x, packet.y, packet.angle).add();
- });
-
- Net.handleClient(BlockDestroyPacket.class, packet -> {
- Tile tile = world.tile(packet.position % world.width(), packet.position / world.width());
- if (tile != null && tile.entity != null) {
- tile.entity.onDeath(true);
- }
- });
-
- Net.handleClient(BlockUpdatePacket.class, packet -> {
- Tile tile = world.tile(packet.position % world.width(), packet.position / world.width());
- if (tile != null && tile.entity != null) {
- tile.entity.health = packet.health;
- }
- });
-
- Net.handleClient(DisconnectPacket.class, packet -> {
- Player player = playerGroup.getByID(packet.playerid);
-
- if (player != null) {
- player.remove();
- }
-
- Platform.instance.updateRPC();
- });
-
- Net.handleClient(KickPacket.class, packet -> {
- kicked = true;
- Net.disconnect();
- state.set(State.menu);
- if(!packet.reason.quiet) ui.showError("$text.server.kicked." + packet.reason.name());
- ui.loadfrag.hide();
- });
-
- Net.handleClient(GameOverPacket.class, packet -> {
- if(world.getCore().block() != ProductionBlocks.core &&
- world.getCore().entity != null){
- world.getCore().entity.onDeath(true);
- }
- kicked = true;
- ui.restart.show();
- });
-
- Net.handleClient(FriendlyFireChangePacket.class, packet -> state.friendlyFire = packet.enabled);
-
- Net.handleClient(ItemTransferPacket.class, packet -> {
- Runnable r = () -> {
- Tile tile = world.tile(packet.position);
- if (tile == null || tile.entity == null) return;
- Tile next = tile.getNearby(packet.rotation);
- tile.entity.items[packet.itemid] --;
- next.block().handleItem(Item.getByID(packet.itemid), next, tile);
- };
-
- threads.run(r);
- });
-
- Net.handleClient(ItemSetPacket.class, packet -> {
- Runnable r = () -> {
- Tile tile = world.tile(packet.position);
- if (tile == null || tile.entity == null) return;
- tile.entity.items[packet.itemid] = packet.amount;
- };
-
- threads.run(r);
- });
-
- Net.handleClient(ItemOffloadPacket.class, packet -> {
- Runnable r = () -> {
- Tile tile = world.tile(packet.position);
- if (tile == null || tile.entity == null) return;
- Tile next = tile.getNearby(tile.getRotation());
- next.block().handleItem(Item.getByID(packet.itemid), next, tile);
- };
-
- threads.run(r);
- });
-
- Net.handleClient(NetErrorPacket.class, packet -> {
- ui.showError(packet.message);
- disconnectQuietly();
- });
-
- Net.handleClient(PlayerAdminPacket.class, packet -> {
- Player player = playerGroup.getByID(packet.id);
- player.isAdmin = packet.admin;
- ui.listfrag.rebuild();
- });
-
- Net.handleClient(TracePacket.class, packet -> {
- Player player = playerGroup.getByID(packet.info.playerid);
- ui.traces.show(player, packet.info);
- });
-
- Net.handleClient(UpgradePacket.class, packet -> {
- Weapon weapon = (Weapon) Upgrade.getByID(packet.id);
-
- state.inventory.removeItems(UpgradeRecipes.get(weapon));
- control.upgrades().addWeapon(weapon);
- ui.hudfrag.updateWeapons();
- Effects.sound("purchase");
- });
+ }catch(IOException e){
+ throw new RuntimeException(e);
+ }
}
@Override
@@ -329,12 +305,12 @@ public class NetClient extends Module {
}else if(!connecting){
Net.disconnect();
}else{ //...must be connecting
- timeoutTime += Timers.delta();
+ timeoutTime += Time.delta();
if(timeoutTime > dataTimeout){
Log.err("Failed to load data!");
ui.loadfrag.hide();
- kicked = true;
- ui.showError("$text.disconnect.data");
+ quiet = true;
+ ui.showError("$disconnect.data");
Net.disconnect();
timeoutTime = 0f;
}
@@ -351,8 +327,20 @@ public class NetClient extends Module {
ui.loadfrag.hide();
ui.join.hide();
Net.setClientLoaded(true);
- Timers.runTask(1f, () -> Net.send(new ConnectConfirmPacket(), SendMode.tcp));
- Timers.runTask(40f, Platform.instance::updateRPC);
+ Core.app.post(Call::connectConfirm);
+ Time.runTask(40f, Platform.instance::updateRPC);
+ }
+
+ private void reset(){
+ Net.setClientLoaded(false);
+ removed.clear();
+ timeoutTime = 0f;
+ connecting = true;
+ quiet = false;
+ lastSent = 0;
+
+ Entities.clear();
+ ui.chatfrag.clearMessages();
}
public void beginConnecting(){
@@ -360,31 +348,60 @@ public class NetClient extends Module {
}
public void disconnectQuietly(){
- kicked = true;
+ quiet = true;
Net.disconnect();
}
- public void clearRecieved(){
- recieved.clear();
+ /** When set, any disconnects will be ignored and no dialogs will be shown. */
+ public void setQuiet(){
+ quiet = true;
+ }
+
+ public void addRemovedEntity(int id){
+ removed.add(id);
+ }
+
+ public boolean isEntityUsed(int id){
+ return removed.contains(id);
}
void sync(){
- requests = 0;
if(timer.get(0, playerSyncTime)){
+ BuildRequest[] requests;
+ //limit to 10 to prevent buffer overflows
+ int usedRequests = Math.min(player.buildQueue().size, 10);
- byte[] bytes = new byte[player.getWriteSize() + 8];
- ByteBuffer buffer = ByteBuffer.wrap(bytes);
- buffer.putLong(TimeUtils.millis());
- player.write(buffer);
+ requests = new BuildRequest[usedRequests];
+ for(int i = 0; i < usedRequests; i++){
+ requests[i] = player.buildQueue().get(i);
+ }
- PositionPacket packet = new PositionPacket();
- packet.data = bytes;
- Net.send(packet, SendMode.udp);
+ Call.onClientShapshot(lastSent++, player.x, player.y,
+ player.pointerX, player.pointerY, player.rotation, player.baseRotation,
+ player.velocity().x, player.velocity().y,
+ player.getMineTile(),
+ player.isBoosting, player.isShooting, ui.chatfrag.chatOpen(),
+ requests,
+ Core.camera.position.x, Core.camera.position.y,
+ Core.camera.width * viewScale, Core.camera.height * viewScale);
}
if(timer.get(1, 60)){
Net.updatePing();
}
}
-}
+
+ String getUsid(String ip){
+ if(Core.settings.getString("usid-" + ip, null) != null){
+ return Core.settings.getString("usid-" + ip, null);
+ }else{
+ byte[] bytes = new byte[8];
+ new RandomXS128().nextBytes(bytes);
+ String result = new String(Base64Coder.encode(bytes));
+ Core.settings.put("usid-" + ip, result);
+ Core.settings.save();
+ return result;
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/src/io/anuke/mindustry/core/NetCommon.java b/core/src/io/anuke/mindustry/core/NetCommon.java
deleted file mode 100644
index 9b8a9435da..0000000000
--- a/core/src/io/anuke/mindustry/core/NetCommon.java
+++ /dev/null
@@ -1,69 +0,0 @@
-package io.anuke.mindustry.core;
-
-import io.anuke.mindustry.entities.Player;
-import io.anuke.mindustry.net.Net;
-import io.anuke.mindustry.net.Net.SendMode;
-import io.anuke.mindustry.net.Packets.*;
-import io.anuke.mindustry.resource.Upgrade;
-import io.anuke.mindustry.resource.Weapon;
-import io.anuke.mindustry.world.Tile;
-import io.anuke.ucore.modules.Module;
-
-import static io.anuke.mindustry.Vars.*;
-
-public class NetCommon extends Module {
-
- public NetCommon(){
-
- Net.handle(ShootPacket.class, (packet) -> {
- Player player = playerGroup.getByID(packet.playerid);
-
- Weapon weapon = (Weapon) Upgrade.getByID(packet.weaponid);
- weapon.shoot(player, packet.x, packet.y, packet.rotation);
- });
-
- Net.handle(ChatPacket.class, (packet) -> {
- ui.chatfrag.addMessage(packet.text, colorizeName(packet.id, packet.name));
- });
-
- Net.handle(WeaponSwitchPacket.class, (packet) -> {
- Player player = playerGroup.getByID(packet.playerid);
-
- if (player == null) return;
-
- player.weaponLeft = (Weapon) Upgrade.getByID(packet.left);
- player.weaponRight = (Weapon) Upgrade.getByID(packet.right);
- });
-
- Net.handle(BlockTapPacket.class, (packet) -> {
- Tile tile = world.tile(packet.position);
- tile.block().tapped(tile);
- });
-
- Net.handle(BlockConfigPacket.class, (packet) -> {
- Tile tile = world.tile(packet.position);
- if (tile != null) tile.block().configure(tile, packet.data);
- });
-
- Net.handle(PlayerDeathPacket.class, (packet) -> {
- Player player = playerGroup.getByID(packet.id);
- if(player == null) return;
-
- player.doRespawn();
- });
- }
-
- public void sendMessage(String message){
- ChatPacket packet = new ChatPacket();
- packet.name = null;
- packet.text = message;
- Net.send(packet, SendMode.tcp);
- if(!headless) ui.chatfrag.addMessage(message, null);
- }
-
- public String colorizeName(int id, String name){
- Player player = playerGroup.getByID(id);
- if(name == null || player == null) return null;
- return "[#" + player.color.toString().toUpperCase() + "]" + name;
- }
-}
diff --git a/core/src/io/anuke/mindustry/core/NetServer.java b/core/src/io/anuke/mindustry/core/NetServer.java
index 0559a441c6..ab4eca7e0d 100644
--- a/core/src/io/anuke/mindustry/core/NetServer.java
+++ b/core/src/io/anuke/mindustry/core/NetServer.java
@@ -1,52 +1,71 @@
package io.anuke.mindustry.core;
-import com.badlogic.gdx.utils.*;
+import io.anuke.annotations.Annotations.Loc;
+import io.anuke.annotations.Annotations.Remote;
+import io.anuke.arc.ApplicationListener;
+import io.anuke.arc.Events;
+import io.anuke.arc.collection.IntMap;
+import io.anuke.arc.collection.ObjectSet;
+import io.anuke.arc.graphics.Color;
+import io.anuke.arc.graphics.Colors;
+import io.anuke.arc.math.Mathf;
+import io.anuke.arc.math.geom.Rectangle;
+import io.anuke.arc.math.geom.Vector2;
+import io.anuke.arc.util.*;
+import io.anuke.arc.util.io.*;
+import io.anuke.mindustry.content.Blocks;
import io.anuke.mindustry.core.GameState.State;
-import io.anuke.mindustry.entities.Player;
-import io.anuke.mindustry.entities.SyncEntity;
-import io.anuke.mindustry.game.EventType.GameOverEvent;
-import io.anuke.mindustry.io.Version;
+import io.anuke.mindustry.entities.Entities;
+import io.anuke.mindustry.entities.EntityGroup;
+import io.anuke.mindustry.entities.traits.BuilderTrait.BuildRequest;
+import io.anuke.mindustry.entities.traits.Entity;
+import io.anuke.mindustry.entities.traits.SyncTrait;
+import io.anuke.mindustry.entities.type.Player;
+import io.anuke.mindustry.game.EventType.WorldLoadEvent;
+import io.anuke.mindustry.game.Team;
+import io.anuke.mindustry.game.Version;
+import io.anuke.mindustry.gen.Call;
+import io.anuke.mindustry.gen.RemoteReadServer;
import io.anuke.mindustry.net.*;
import io.anuke.mindustry.net.Administration.PlayerInfo;
-import io.anuke.mindustry.net.Net.SendMode;
+import io.anuke.mindustry.net.Administration.TraceInfo;
import io.anuke.mindustry.net.Packets.*;
-import io.anuke.mindustry.resource.*;
-import io.anuke.mindustry.world.Block;
-import io.anuke.mindustry.world.Placement;
import io.anuke.mindustry.world.Tile;
-import io.anuke.ucore.core.Events;
-import io.anuke.ucore.core.Timers;
-import io.anuke.ucore.entities.Entities;
-import io.anuke.ucore.entities.EntityGroup;
-import io.anuke.ucore.modules.Module;
-import io.anuke.ucore.util.Log;
-import io.anuke.ucore.util.Timer;
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
+import java.io.*;
import java.nio.ByteBuffer;
+import java.util.zip.DeflaterOutputStream;
+
import static io.anuke.mindustry.Vars.*;
-public class NetServer extends Module{
- private final static float serverSyncTime = 4, itemSyncTime = 10, kickDuration = 30 * 1000;
-
- private final static int timerEntitySync = 0;
- private final static int timerStateSync = 1;
+public class NetServer implements ApplicationListener{
+ public final static int maxSnapshotSize = 430;
+ private final static float serverSyncTime = 15, kickDuration = 30 * 1000;
+ private final static Vector2 vector = new Vector2();
+ private final static Rectangle viewport = new Rectangle();
+ /** If a player goes away of their server-side coordinates by this distance, they get teleported back. */
+ private final static float correctDist = 16f;
public final Administration admins = new Administration();
- /**Maps connection IDs to players.*/
+ /** Maps connection IDs to players. */
private IntMap connections = new IntMap<>();
- private ObjectMap weapons = new ObjectMap<>();
private boolean closing = false;
- private Timer timer = new Timer(5);
+
+ private ByteBuffer writeBuffer = ByteBuffer.allocate(127);
+ private ByteBufferOutput outputBuffer = new ByteBufferOutput(writeBuffer);
+
+ /** Stream for writing player sync data to. */
+ private ReusableByteOutStream syncStream = new ReusableByteOutStream();
+ /** Data stream for writing player sync data to. */
+ private DataOutputStream dataStream = new DataOutputStream(syncStream);
public NetServer(){
-
- Events.on(GameOverEvent.class, () -> {
- weapons.clear();
- admins.getEditLogs().clear();
- });
+ Events.on(WorldLoadEvent.class, event -> {
+ if(!headless){
+ connections.clear();
+ }
+ });
Net.handleServer(Connect.class, (id, connect) -> {
if(admins.isIPBanned(connect.addressTCP)){
@@ -54,28 +73,72 @@ public class NetServer extends Module{
}
});
+ Net.handleServer(Disconnect.class, (id, packet) -> {
+ Player player = connections.get(id);
+ if(player != null){
+ onDisconnect(player);
+ }
+ connections.remove(id);
+ });
+
Net.handleServer(ConnectPacket.class, (id, packet) -> {
- String uuid = new String(Base64Coder.encode(packet.uuid));
+ String uuid = packet.uuid;
- if(Net.getConnection(id) == null ||
- admins.isIPBanned(Net.getConnection(id).address)) return;
+ NetConnection connection = Net.getConnection(id);
+
+ if(connection == null ||
+ admins.isIPBanned(connection.address)) return;
+
+ if(connection.hasBegunConnecting){
+ kick(id, KickReason.idInUse);
+ return;
+ }
+
+ connection.hasBegunConnecting = true;
- TraceInfo trace = admins.getTrace(Net.getConnection(id).address);
PlayerInfo info = admins.getInfo(uuid);
- trace.uuid = uuid;
- trace.android = packet.android;
+
+ connection.mobile = packet.mobile;
if(admins.isIDBanned(uuid)){
kick(id, KickReason.banned);
return;
}
- if(TimeUtils.millis() - info.lastKicked < kickDuration){
+ if(Time.millis() - info.lastKicked < kickDuration){
kick(id, KickReason.recentKick);
return;
}
- Log.info("Recieved connect packet for player '{0}' / UUID {1} / IP {2}", packet.name, uuid, trace.ip);
+ if(packet.versionType == null || ((packet.version == -1 || !packet.versionType.equals(Version.type)) && Version.build != -1 && !admins.allowsCustomClients())){
+ kick(id, KickReason.customClient);
+ return;
+ }
+
+ boolean preventDuplicates = headless && netServer.admins.getStrict();
+
+ if(preventDuplicates){
+ for(Player player : playerGroup.all()){
+ if(player.name.trim().equalsIgnoreCase(packet.name.trim())){
+ kick(id, KickReason.nameInUse);
+ return;
+ }
+
+ if(player.uuid.equals(packet.uuid) || player.usid.equals(packet.usid)){
+ kick(id, KickReason.idInUse);
+ return;
+ }
+ }
+ }
+
+ packet.name = fixName(packet.name);
+
+ if(packet.name.trim().length() <= 0){
+ kick(id, KickReason.nameEmpty);
+ return;
+ }
+
+ Log.debug("Recieved connect packet for player '{0}' / UUID {1} / IP {2}", packet.name, uuid, connection.address);
String ip = Net.getConnection(id).address;
@@ -87,294 +150,250 @@ public class NetServer extends Module{
}
if(packet.version == -1){
- trace.modclient = true;
+ connection.modclient = true;
}
Player player = new Player();
- player.isAdmin = admins.isAdmin(uuid, ip);
- player.clientid = id;
+ player.isAdmin = admins.isAdmin(uuid, packet.usid);
+ player.con = Net.getConnection(id);
+ player.usid = packet.usid;
player.name = packet.name;
- player.isAndroid = packet.android;
- player.set(world.getSpawnX(), world.getSpawnY());
- player.setNet(player.x, player.y);
+ player.uuid = uuid;
+ player.isMobile = packet.mobile;
+ player.dead = true;
player.setNet(player.x, player.y);
player.color.set(packet.color);
+ player.color.a = 1f;
+
+ try{
+ writeBuffer.position(0);
+ player.write(outputBuffer);
+ }catch(Throwable t){
+ t.printStackTrace();
+ kick(id, KickReason.nameEmpty);
+ return;
+ }
+
+ //playing in pvp mode automatically assigns players to teams
+ if(state.rules.pvp){
+ player.setTeam(assignTeam(playerGroup.all()));
+ Log.info("Auto-assigned player {0} to team {1}.", player.name, player.getTeam());
+ }
+
connections.put(id, player);
- trace.playerid = player.id;
-
- if(world.getMap().custom){
- ByteArrayOutputStream stream = new ByteArrayOutputStream();
- NetworkIO.writeMap(world.getMap(), stream);
- CustomMapPacket data = new CustomMapPacket();
- data.stream = new ByteArrayInputStream(stream.toByteArray());
- Net.sendStream(id, data);
-
- Log.info("Sending custom map: Packed {0} uncompressed bytes of MAP data.", stream.size());
- }else{
- //hack-- simulate the map ack packet recieved to send the world data to the client.
- Net.handleServerReceived(id, new MapAckPacket());
- }
+ sendWorldData(player, id);
Platform.instance.updateRPC();
});
- Net.handleServer(MapAckPacket.class, (id, packet) -> {
+ Net.handleServer(InvokePacket.class, (id, packet) -> {
Player player = connections.get(id);
-
- ByteArrayOutputStream stream = new ByteArrayOutputStream();
- NetworkIO.writeWorld(player, weapons.get(admins.getTrace(Net.getConnection(id).address).uuid, new ByteArray()), stream);
- WorldData data = new WorldData();
- data.stream = new ByteArrayInputStream(stream.toByteArray());
- Net.sendStream(id, data);
-
- Log.info("Packed {0} uncompressed bytes of WORLD data.", stream.size());
- });
-
- Net.handleServer(ConnectConfirmPacket.class, (id, packet) -> {
- Player player = connections.get(id);
-
- if (player == null) return;
-
- player.add();
- Log.info("&y{0} has connected.", player.name);
- netCommon.sendMessage("[accent]" + player.name + " has connected.");
- });
-
- Net.handleServer(Disconnect.class, (id, packet) -> {
- Player player = connections.get(packet.id);
-
- if (player == null) {
- Log.err("Unknown client has disconnected (ID={0})", id);
- return;
- }
-
- Log.info("&y{0} has disconnected.", player.name);
- netCommon.sendMessage("[accent]" + player.name + " has disconnected.");
- player.remove();
-
- DisconnectPacket dc = new DisconnectPacket();
- dc.playerid = player.id;
-
- Net.send(dc, SendMode.tcp);
-
- Platform.instance.updateRPC();
- admins.save();
- });
-
- Net.handleServer(PositionPacket.class, (id, packet) -> {
- ByteBuffer buffer = ByteBuffer.wrap(packet.data);
- long time = buffer.getLong();
-
- Player player = connections.get(id);
- player.read(buffer, time);
- });
-
- Net.handleServer(ShootPacket.class, (id, packet) -> {
- TraceInfo info = admins.getTrace(Net.getConnection(id).address);
- Weapon weapon = (Weapon)Upgrade.getByID(packet.weaponid);
-
- float wtrc = 80;
-
- if(!Timers.get("fastshoot-" + id + "-" + weapon.id, wtrc)){
- info.fastShots.getAndIncrement(weapon.id, 0, 1);
-
- if(info.fastShots.get(weapon.id, 0) > (int)(wtrc / (weapon.getReload() / 2f)) + 30){
- kick(id, KickReason.fastShoot);
- }
- }else{
- info.fastShots.put(weapon.id, 0);
- }
-
- packet.playerid = connections.get(id).id;
- Net.sendExcept(id, packet, SendMode.udp);
- });
-
- Net.handleServer(PlacePacket.class, (id, packet) -> {
- packet.playerid = connections.get(id).id;
-
- Block block = Block.getByID(packet.block);
-
- if(!Placement.validPlace(packet.x, packet.y, block)) return;
-
- Recipe recipe = Recipes.getByResult(block);
-
- if(recipe == null) return;
-
- Tile tile = world.tile(packet.x, packet.y);
- if(tile.synthetic() && admins.isValidateReplace() && !admins.validateBreak(admins.getTrace(Net.getConnection(id).address).uuid, Net.getConnection(id).address)){
- if(Timers.get("break-message-" + id, 120)){
- sendMessageTo(id, "[scarlet]Anti-grief: you are replacing blocks too quickly. wait until replacing again.");
- }
- return;
- }
-
- state.inventory.removeItems(recipe.requirements);
-
- Placement.placeBlock(packet.x, packet.y, block, packet.rotation, true, false);
-
- admins.logEdit(packet.x, packet.y, connections.get(id), block, packet.rotation, EditLog.EditAction.PLACE);
- admins.getTrace(Net.getConnection(id).address).lastBlockPlaced = block;
- admins.getTrace(Net.getConnection(id).address).totalBlocksPlaced ++;
- admins.getInfo(admins.getTrace(Net.getConnection(id).address).uuid).totalBlockPlaced ++;
-
- Net.send(packet, SendMode.tcp);
- });
-
- Net.handleServer(BreakPacket.class, (id, packet) -> {
- packet.playerid = connections.get(id).id;
-
- if(!Placement.validBreak(packet.x, packet.y)) return;
-
- Tile tile = world.tile(packet.x, packet.y);
-
- if(tile.synthetic() && !admins.validateBreak(admins.getTrace(Net.getConnection(id).address).uuid, Net.getConnection(id).address)){
- if(Timers.get("break-message-" + id, 120)){
- sendMessageTo(id, "[scarlet]Anti-grief: you are breaking blocks too quickly. wait until breaking again.");
- }
- return;
- }
-
- Block block = Placement.breakBlock(packet.x, packet.y, true, false);
-
- if(block != null) {
- admins.logEdit(packet.x, packet.y, connections.get(id), block, tile.getRotation(), EditLog.EditAction.BREAK);
- admins.getTrace(Net.getConnection(id).address).lastBlockBroken = block;
- admins.getTrace(Net.getConnection(id).address).totalBlocksBroken++;
- admins.getInfo(admins.getTrace(Net.getConnection(id).address).uuid).totalBlocksBroken ++;
- if (block.update || block.destructible)
- admins.getTrace(Net.getConnection(id).address).structureBlocksBroken++;
- }
-
- Net.send(packet, SendMode.tcp);
- });
-
- Net.handleServer(ChatPacket.class, (id, packet) -> {
- if(!Timers.get("chatFlood" + id, 20)){
- ChatPacket warn = new ChatPacket();
- warn.text = "[scarlet]You are sending messages too quickly.";
- Net.sendTo(id, warn, SendMode.tcp);
- return;
- }
- Player player = connections.get(id);
- packet.name = player.name;
- packet.id = player.id;
- Net.send(packet, SendMode.tcp);
- });
-
- Net.handleServer(UpgradePacket.class, (id, packet) -> {
- Player player = connections.get(id);
-
- Weapon weapon = (Weapon) Upgrade.getByID(packet.id);
- String uuid = admins.getTrace(Net.getConnection(id).address).uuid;
-
- if(!state.inventory.hasItems(UpgradeRecipes.get(weapon))){
- return;
- }
-
- if (!weapons.containsKey(uuid)) weapons.put(uuid, new ByteArray());
-
- if (!weapons.get(uuid).contains(weapon.id)){
- weapons.get(uuid).add(weapon.id);
- }else{
- return;
- }
-
- state.inventory.removeItems(UpgradeRecipes.get(weapon));
- Net.sendTo(id, packet, SendMode.tcp);
- });
-
- Net.handleServer(WeaponSwitchPacket.class, (id, packet) -> {
- TraceInfo info = admins.getTrace(Net.getConnection(id).address);
-
- packet.playerid = connections.get(id).id;
- Net.sendExcept(id, packet, SendMode.tcp);
- });
-
- Net.handleServer(BlockTapPacket.class, (id, packet) -> {
- Net.sendExcept(id, packet, SendMode.tcp);
- });
-
- Net.handleServer(BlockConfigPacket.class, (id, packet) -> {
- Net.sendExcept(id, packet, SendMode.tcp);
- });
-
- Net.handleServer(EntityRequestPacket.class, (cid, packet) -> {
-
- int id = packet.id;
- int dest = cid;
- EntityGroup group = Entities.getGroup(packet.group);
- if(group.getByID(id) != null){
- EntitySpawnPacket p = new EntitySpawnPacket();
- p.entity = (SyncEntity)group.getByID(id);
- p.group = group;
- Net.sendTo(dest, p, SendMode.tcp);
- }
- });
-
- Net.handleServer(PlayerDeathPacket.class, (id, packet) -> {
- packet.id = connections.get(id).id;
- Net.sendExcept(id, packet, SendMode.tcp);
- });
-
- Net.handleServer(AdministerRequestPacket.class, (id, packet) -> {
- Player player = connections.get(id);
-
- if(!player.isAdmin){
- Log.err("ACCESS DENIED: Player {0} / {1} attempted to perform admin action without proper security access.",
- player.name, Net.getConnection(player.clientid).address);
- return;
- }
-
- Player other = playerGroup.getByID(packet.id);
-
- if(other == null || other.isAdmin){
- Log.err("{0} attempted to perform admin action on nonexistant or admin player.", player.name);
- return;
- }
-
- String ip = Net.getConnection(other.clientid).address;
-
- if(packet.action == AdminAction.ban){
- admins.banPlayerIP(ip);
- kick(other.clientid, KickReason.banned);
- Log.info("&lc{0} has banned {1}.", player.name, other.name);
- }else if(packet.action == AdminAction.kick){
- kick(other.clientid, KickReason.kick);
- Log.info("&lc{0} has kicked {1}.", player.name, other.name);
- }else if(packet.action == AdminAction.trace){
- TracePacket trace = new TracePacket();
- trace.info = admins.getTrace(ip);
- Net.sendTo(id, trace, SendMode.tcp);
- Log.info("&lc{0} has requested trace info of {1}.", player.name, other.name);
- }
- });
-
- Net.handleServer(BlockLogRequestPacket.class, (id, packet) -> {
- packet.editlogs = admins.getEditLogs().get(packet.x + packet.y * world.width(), new Array<>());
- Net.sendTo(id, packet, SendMode.udp);
- });
-
- Net.handleServer(RollbackRequestPacket.class, (id, packet) -> {
- Player player = connections.get(id);
-
- if(!player.isAdmin){
- Log.err("ACCESS DENIED: Player {0} / {1} attempted to perform a rollback without proper security access.",
- player.name, Net.getConnection(player.clientid).address);
- return;
- }
-
- admins.rollbackWorld(packet.rollbackTimes);
- Log.info("&lc{0} has rolled back the world {1} times.", player.name, packet.rollbackTimes);
+ if(player == null) return;
+ RemoteReadServer.readPacket(packet.writeBuffer, packet.type, player);
});
}
+ public Team assignTeam(Iterable players){
+ //find team with minimum amount of players and auto-assign player to that.
+ return Structs.findMin(Team.all, team -> {
+ if(state.teams.isActive(team) && !state.teams.get(team).cores.isEmpty()){
+ int count = 0;
+ for(Player other : players){
+ if(other.getTeam() == team){
+ count++;
+ }
+ }
+ return count;
+ }
+ return Integer.MAX_VALUE;
+ });
+ }
+
+ public void sendWorldData(Player player, int clientID){
+ ByteArrayOutputStream stream = new ByteArrayOutputStream();
+ DeflaterOutputStream def = new FastDeflaterOutputStream(stream);
+ NetworkIO.writeWorld(player, def);
+ WorldStream data = new WorldStream();
+ data.stream = new ByteArrayInputStream(stream.toByteArray());
+ Net.sendStream(clientID, data);
+
+ Log.debug("Packed {0} compressed bytes of world data.", stream.size());
+ }
+
+ public static void onDisconnect(Player player){
+ //singleplayer multiplayer wierdness
+ if(player.con == null){
+ player.remove();
+ return;
+ }
+
+ if(player.con.hasConnected){
+ Call.sendMessage("[accent]" + player.name + "[accent] has disconnected.");
+ Call.onPlayerDisconnect(player.id);
+ }
+ player.remove();
+ netServer.connections.remove(player.con.id);
+ Log.info("&lm[{1}] &lc{0} has disconnected.", player.name, player.uuid);
+ }
+
+ private static float compound(float speed, float drag){
+ float total = 0f;
+ for(int i = 0; i < 50; i++){
+ total *= (1f - drag);
+ total += speed;
+ }
+ return total;
+ }
+
+ @Remote(targets = Loc.client, unreliable = true)
+ public static void onClientShapshot(
+ Player player,
+ int snapshotID,
+ float x, float y,
+ float pointerX, float pointerY,
+ float rotation, float baseRotation,
+ float xVelocity, float yVelocity,
+ Tile mining,
+ boolean boosting, boolean shooting, boolean chatting,
+ BuildRequest[] requests,
+ float viewX, float viewY, float viewWidth, float viewHeight
+ ){
+ NetConnection connection = player.con;
+ if(connection == null || snapshotID < connection.lastRecievedClientSnapshot) return;
+
+ boolean verifyPosition = !player.isDead() && netServer.admins.getStrict() && headless;
+
+ if(connection.lastRecievedClientTime == 0) connection.lastRecievedClientTime = Time.millis() - 16;
+
+ connection.viewX = viewX;
+ connection.viewY = viewY;
+ connection.viewWidth = viewWidth;
+ connection.viewHeight = viewHeight;
+
+ long elapsed = Time.timeSinceMillis(connection.lastRecievedClientTime);
+
+ float maxSpeed = boosting && !player.mech.flying ? player.mech.boostSpeed : player.mech.speed;
+ float maxMove = elapsed / 1000f * 60f * Math.min(compound(maxSpeed, player.mech.drag) * 1.25f, player.mech.maxSpeed * 1.1f);
+
+ player.pointerX = pointerX;
+ player.pointerY = pointerY;
+ player.setMineTile(mining);
+ player.isTyping = chatting;
+ player.isBoosting = boosting;
+ player.isShooting = shooting;
+ player.buildQueue().clear();
+ for(BuildRequest req : requests){
+ Tile tile = world.tile(req.x, req.y);
+ if(tile == null) continue;
+ //auto-skip done requests
+ if(req.breaking && tile.block() == Blocks.air){
+ continue;
+ }else if(!req.breaking && tile.block() == req.block && (!req.block.rotate || tile.rotation() == req.rotation)){
+ continue;
+ }
+ player.buildQueue().addLast(req);
+ }
+
+ vector.set(x - player.getInterpolator().target.x, y - player.getInterpolator().target.y);
+ //vector.limit(maxMove);
+
+ float prevx = player.x, prevy = player.y;
+ player.set(player.getInterpolator().target.x, player.getInterpolator().target.y);
+ if(!player.mech.flying && player.boostHeat < 0.01f){
+ player.move(vector.x, vector.y);
+ }else{
+ player.x += vector.x;
+ player.y += vector.y;
+ }
+ float newx = player.x, newy = player.y;
+
+ if(!verifyPosition){
+ player.x = prevx;
+ player.y = prevy;
+ newx = x;
+ newy = y;
+ }else if(Mathf.dst(x, y, newx, newy) > correctDist){
+ Call.onPositionSet(player.con.id, newx, newy); //teleport and correct position when necessary
+ }
+
+ //reset player to previous synced position so it gets interpolated
+ player.x = prevx;
+ player.y = prevy;
+
+ //set interpolator target to *new* position so it moves toward it
+ player.getInterpolator().read(player.x, player.y, newx, newy, rotation, baseRotation);
+ player.velocity().set(xVelocity, yVelocity); //only for visual calculation purposes, doesn't actually update the player
+
+ connection.lastRecievedClientSnapshot = snapshotID;
+ connection.lastRecievedClientTime = Time.millis();
+ }
+
+ @Remote(targets = Loc.client, called = Loc.server)
+ public static void onAdminRequest(Player player, Player other, AdminAction action){
+
+ if(!player.isAdmin){
+ Log.warn("ACCESS DENIED: Player {0} / {1} attempted to perform admin action without proper security access.",
+ player.name, player.con.address);
+ return;
+ }
+
+ if(other == null || ((other.isAdmin && !player.isLocal) && other != player)){
+ Log.warn("{0} attempted to perform admin action on nonexistant or admin player.", player.name);
+ return;
+ }
+
+ if(action == AdminAction.wave){
+ //no verification is done, so admins can hypothetically spam waves
+ //not a real issue, because server owners may want to do just that
+ state.wavetime = 0f;
+ }else if(action == AdminAction.ban){
+ netServer.admins.banPlayerIP(other.con.address);
+ netServer.kick(other.con.id, KickReason.banned);
+ Log.info("&lc{0} has banned {1}.", player.name, other.name);
+ }else if(action == AdminAction.kick){
+ netServer.kick(other.con.id, KickReason.kick);
+ Log.info("&lc{0} has kicked {1}.", player.name, other.name);
+ }else if(action == AdminAction.trace){
+ TraceInfo info = new TraceInfo(other.con.address, other.uuid, other.con.modclient, other.con.mobile);
+ if(player.con != null){
+ Call.onTraceInfo(player.con.id, other, info);
+ }else{
+ NetClient.onTraceInfo(other, info);
+ }
+ Log.info("&lc{0} has requested trace info of {1}.", player.name, other.name);
+ }
+ }
+
+ @Remote(targets = Loc.client)
+ public static void connectConfirm(Player player){
+ if(player.con == null || player.con.hasConnected) return;
+
+ player.add();
+ player.con.hasConnected = true;
+ Call.sendMessage("[accent]" + player.name + "[accent] has connected.");
+ Log.info("&lm[{1}] &y{0} has connected. ", player.name, player.uuid);
+ }
+
+ public boolean isWaitingForPlayers(){
+ if(state.rules.pvp){
+ int used = 0;
+ for(Team t : Team.all){
+ if(playerGroup.count(p -> p.getTeam() == t) > 0){
+ used++;
+ }
+ }
+ return used < 2;
+ }
+ return false;
+ }
+
public void update(){
+
if(!headless && !closing && Net.server() && state.is(State.menu)){
closing = true;
- reset();
- ui.loadfrag.show("$text.server.closing");
- Timers.runTask(5f, () -> {
+ ui.loadfrag.show("$server.closing");
+ Time.runTask(5f, () -> {
Net.closeServer();
ui.loadfrag.hide();
closing = false;
@@ -386,9 +405,10 @@ public class NetServer extends Module{
}
}
- public void reset(){
- weapons.clear();
- admins.clearTraces();
+ public void kickAll(KickReason reason){
+ for(NetConnection con : Net.getConnections()){
+ kick(con.id, reason);
+ }
}
public void kick(int connection, KickReason reason){
@@ -397,107 +417,159 @@ public class NetServer extends Module{
Log.err("Cannot kick unknown player!");
return;
}else{
- Log.info("Kicking connection #{0} / IP: {1}. Reason: {2}", connection, con.address, reason);
+ Log.info("Kicking connection #{0} / IP: {1}. Reason: {2}", connection, con.address, reason.name());
}
- if((reason == KickReason.kick || reason == KickReason.banned) && admins.getTrace(con.address).uuid != null){
- PlayerInfo info = admins.getInfo(admins.getTrace(con.address).uuid);
- info.timesKicked ++;
- info.lastKicked = TimeUtils.millis();
+ Player player = connections.get(con.id);
+
+ if(player != null && (reason == KickReason.kick || reason == KickReason.banned) && player.uuid != null){
+ PlayerInfo info = admins.getInfo(player.uuid);
+ info.timesKicked++;
+ info.lastKicked = Time.millis();
}
- KickPacket p = new KickPacket();
- p.reason = reason;
+ Call.onKick(connection, reason);
- con.send(p, SendMode.tcp);
- Timers.runTask(2f, con::close);
+ Time.runTask(2f, con::close);
admins.save();
}
- void sendMessageTo(int id, String message){
- ChatPacket packet = new ChatPacket();
- packet.text = message;
- Net.sendTo(id, packet, SendMode.tcp);
+ public void writeSnapshot(Player player) throws IOException{
+ syncStream.reset();
+ ObjectSet cores = state.teams.get(player.getTeam()).cores;
+
+ dataStream.writeByte(cores.size);
+
+ for(Tile tile : cores){
+ dataStream.writeInt(tile.pos());
+ tile.entity.items.write(dataStream);
+ }
+
+ dataStream.close();
+ byte[] stateBytes = syncStream.toByteArray();
+
+ //write basic state data.
+ Call.onStateSnapshot(player.con.id, state.wavetime, state.wave, state.enemies(), (short)stateBytes.length, Net.compressSnapshot(stateBytes));
+
+ viewport.setSize(player.con.viewWidth, player.con.viewHeight).setCenter(player.con.viewX, player.con.viewY);
+
+ //check for syncable groups
+ for(EntityGroup> group : Entities.getAllGroups()){
+ if(group.isEmpty() || !(group.all().get(0) instanceof SyncTrait)) continue;
+
+ //make sure mapping is enabled for this group
+ if(!group.mappingEnabled()){
+ throw new RuntimeException("Entity group '" + group.getType() + "' contains SyncTrait entities, yet mapping is not enabled. In order for syncing to work, you must enable mapping for this group.");
+ }
+
+ syncStream.reset();
+
+ int sent = 0;
+
+ for(Entity entity : group.all()){
+ SyncTrait sync = (SyncTrait)entity;
+ if(!sync.isSyncing()) continue;
+
+ //write all entities now
+ dataStream.writeInt(entity.getID()); //write id
+ dataStream.writeByte(sync.getTypeID()); //write type ID
+ sync.write(dataStream); //write entity
+
+ sent++;
+
+ if(syncStream.size() > maxSnapshotSize){
+ dataStream.close();
+ byte[] syncBytes = syncStream.toByteArray();
+ Call.onEntitySnapshot(player.con.id, (byte)group.getID(), (short)sent, (short)syncBytes.length, Net.compressSnapshot(syncBytes));
+ sent = 0;
+ syncStream.reset();
+ }
+ }
+
+ if(sent > 0){
+ dataStream.close();
+
+ byte[] syncBytes = syncStream.toByteArray();
+ Call.onEntitySnapshot(player.con.id, (byte)group.getID(), (short)sent, (short)syncBytes.length, Net.compressSnapshot(syncBytes));
+ }
+ }
+ }
+
+ String fixName(String name){
+ name = name.trim();
+ if(name.equals("[") || name.equals("]")){
+ return "";
+ }
+
+ for(int i = 0; i < name.length(); i++){
+ if(name.charAt(i) == '[' && i != name.length() - 1 && name.charAt(i + 1) != '[' && (i == 0 || name.charAt(i - 1) != '[')){
+ String prev = name.substring(0, i);
+ String next = name.substring(i);
+ String result = checkColor(next);
+
+ name = prev + result;
+ }
+ }
+
+ StringBuilder result = new StringBuilder();
+ int curChar = 0;
+ while(curChar < name.length() && result.toString().getBytes().length < maxNameLength){
+ result.append(name.charAt(curChar++));
+ }
+ return result.toString();
+ }
+
+ String checkColor(String str){
+
+ for(int i = 1; i < str.length(); i++){
+ if(str.charAt(i) == ']'){
+ String color = str.substring(1, i);
+
+ if(Colors.get(color.toUpperCase()) != null || Colors.get(color.toLowerCase()) != null){
+ Color result = (Colors.get(color.toLowerCase()) == null ? Colors.get(color.toUpperCase()) : Colors.get(color.toLowerCase()));
+ if(result.a <= 0.8f){
+ return str.substring(i + 1);
+ }
+ }else{
+ try{
+ Color result = Color.valueOf(color);
+ if(result.a <= 0.8f){
+ return str.substring(i + 1);
+ }
+ }catch(Exception e){
+ return str;
+ }
+ }
+ }
+ }
+ return str;
}
void sync(){
- if(timer.get(timerEntitySync, serverSyncTime)){
- //scan through all groups with syncable entities
- for(EntityGroup> group : Entities.getAllGroups()) {
- if(group.size() == 0 || !(group.all().iterator().next() instanceof SyncEntity)) continue;
+ try{
- //get write size for one entity (adding 4, as you need to write the ID as well)
- int writesize = SyncEntity.getWriteSize((Class extends SyncEntity>)group.getType()) + 4;
- //amount of entities
- int amount = group.size();
- //maximum amount of entities per packet
- int maxsize = 64;
+ //iterate through each player
+ for(int i = 0; i < playerGroup.size(); i++){
+ Player player = playerGroup.all().get(i);
+ if(player.isLocal) continue;
- //current buffer you're writing to
- ByteBuffer current = null;
- //number of entities written to this packet/buffer
- int written = 0;
+ NetConnection connection = player.con;
- //for all the entities...
- for (int i = 0; i < amount; i++) {
- //if the buffer is null, create a new one
- if(current == null){
- //calculate amount of entities to go into this packet
- int csize = Math.min(amount-i, maxsize);
- //create a byte array to write to
- byte[] bytes = new byte[csize*writesize + 1 + 8];
- //wrap it for easy writing
- current = ByteBuffer.wrap(bytes);
- current.putLong(TimeUtils.millis());
- //write the group ID so the client knows which group this is
- current.put((byte)group.getID());
- }
-
- SyncEntity entity = (SyncEntity) group.all().get(i);
-
- //write ID to the buffer
- current.putInt(entity.id);
-
- int previous = current.position();
- //write extra data to the buffer
- entity.write(current);
-
- written ++;
-
- //if the packet is too big now...
- if(written >= maxsize){
- //send the packet.
- SyncPacket packet = new SyncPacket();
- packet.data = current.array();
- Net.send(packet, SendMode.udp);
-
- //reset data, send the next packet
- current = null;
- written = 0;
- }
+ if(connection == null || !connection.isConnected() || !connections.containsKey(connection.id)){
+ //player disconnected, call d/c event
+ onDisconnect(player);
+ return;
}
- //make sure to send incomplete packets too
- if(current != null){
- SyncPacket packet = new SyncPacket();
- packet.data = current.array();
- Net.send(packet, SendMode.udp);
- }
+ if(!player.timer.get(Player.timerSync, serverSyncTime) || !connection.hasConnected) continue;
+
+ writeSnapshot(player);
}
- }
- if(timer.get(timerStateSync, itemSyncTime)){
- StateSyncPacket packet = new StateSyncPacket();
- packet.items = state.inventory.getItems();
- packet.countdown = state.wavetime;
- packet.enemies = state.enemies;
- packet.wave = state.wave;
- packet.time = Timers.time();
- packet.timestamp = TimeUtils.millis();
-
- Net.send(packet, SendMode.udp);
+ }catch(IOException e){
+ e.printStackTrace();
}
}
}
diff --git a/core/src/io/anuke/mindustry/core/Platform.java b/core/src/io/anuke/mindustry/core/Platform.java
index 1c3ab23003..85026c9335 100644
--- a/core/src/io/anuke/mindustry/core/Platform.java
+++ b/core/src/io/anuke/mindustry/core/Platform.java
@@ -1,89 +1,91 @@
package io.anuke.mindustry.core;
-import com.badlogic.gdx.files.FileHandle;
-import com.badlogic.gdx.utils.Base64Coder;
-import io.anuke.mindustry.core.ThreadHandler.ThreadProvider;
-import io.anuke.ucore.core.Settings;
-import io.anuke.ucore.entities.Entity;
-import io.anuke.ucore.entities.EntityGroup;
-import io.anuke.ucore.function.Consumer;
-import io.anuke.ucore.scene.ui.TextField;
+import io.anuke.arc.Core;
+import io.anuke.arc.Input.TextInput;
+import io.anuke.arc.files.FileHandle;
+import io.anuke.arc.function.Consumer;
+import io.anuke.arc.function.Predicate;
+import io.anuke.arc.math.RandomXS128;
+import io.anuke.arc.scene.ui.TextField;
+import io.anuke.arc.util.serialization.Base64Coder;
-import java.util.Date;
-import java.util.Locale;
-import java.util.Random;
+import static io.anuke.mindustry.Vars.mobile;
-public abstract class Platform {
- /**Each separate game platform should set this instance to their own implementation.*/
- public static Platform instance = new Platform() {};
+public abstract class Platform{
+ /** Each separate game platform should set this instance to their own implementation. */
+ public static Platform instance = new Platform(){
+ };
- /**Format the date using the default date formatter.*/
- public String format(Date date){return "invalid";}
- /**Format a number by adding in commas or periods where needed.*/
- public String format(int number){return "invalid";}
- /**Show a native error dialog.*/
- public void showError(String text){}
- /**Add a text input dialog that should show up after the field is tapped.*/
- public void addDialog(TextField field){
- addDialog(field, 16);
- }
- /**See addDialog().*/
- public void addDialog(TextField field, int maxLength){}
- /**Update discord RPC.*/
- public void updateRPC(){}
- /**Called when the game is exited.*/
- public void onGameExit(){}
- /**Open donation dialog. Currently android only.*/
- public void openDonations(){}
- /**Whether discord RPC is supported.*/
- public boolean hasDiscord(){return true;}
- /**Request Android permissions for writing files.*/
- public void requestWritePerms(){}
- /**Return the localized name for the locale. This is basically a workaround for GWT not supporting getName().*/
- public String getLocaleName(Locale locale){
- return locale.toString();
- }
- /**Whether joining games is supported.*/
- public boolean canJoinGame(){
- return true;
- }
- /**Whether debug mode is enabled.*/
- public boolean isDebug(){return false;}
- /**Must be 8 bytes in length.*/
- public byte[] getUUID(){
- String uuid = Settings.getString("uuid", "");
- if(uuid.isEmpty()){
- byte[] result = new byte[8];
- new Random().nextBytes(result);
- uuid = new String(Base64Coder.encode(result));
- Settings.putString("uuid", uuid);
- Settings.save();
- return result;
- }
- return Base64Coder.decode(uuid);
- }
- /**Only used for iOS or android: open the share menu for a map or save.*/
- public void shareFile(FileHandle file){}
+ /** Add a text input dialog that should show up after the field is tapped. */
+ public void addDialog(TextField field){
+ addDialog(field, 16);
+ }
- /**Show a file chooser. Desktop only.
- *
+ /** See addDialog(). */
+ public void addDialog(TextField field, int maxLength){
+ if(!mobile) return; //this is mobile only, desktop doesn't need dialogs
+
+ field.tapped(() -> {
+ TextInput input = new TextInput();
+ input.text = field.getText();
+ input.maxLength = maxLength;
+ input.accepted = text -> {
+ field.clearText();
+ field.appendText(text);
+ field.change();
+ Core.input.setOnscreenKeyboardVisible(false);
+ };
+ Core.input.getTextInput(input);
+ });
+ }
+
+ /** Update discord RPC. */
+ public void updateRPC(){
+ }
+
+ /** Whether donating is supported. */
+ public boolean canDonate(){
+ return false;
+ }
+
+ /** Must be a base64 string 8 bytes in length. */
+ public String getUUID(){
+ String uuid = Core.settings.getString("uuid", "");
+ if(uuid.isEmpty()){
+ byte[] result = new byte[8];
+ new RandomXS128().nextBytes(result);
+ uuid = new String(Base64Coder.encode(result));
+ Core.settings.put("uuid", uuid);
+ Core.settings.save();
+ return uuid;
+ }
+ return uuid;
+ }
+
+ /** Only used for iOS or android: open the share menu for a map or save. */
+ public void shareFile(FileHandle file){
+ }
+
+ /**
+ * Show a file chooser.
* @param text File chooser title text
- * @param content Type of files to be loaded
+ * @param content Description of the type of files to be loaded
* @param cons Selection listener
- * @param open Whether to open or save files.
- * @param filetype File extensions to filter.
+ * @param open Whether to open or save files
+ * @param filetype File extension to filter
*/
- public void showFileChooser(String text, String content, Consumer cons, boolean open, String filetype){}
- /**Use the default thread provider from the kryonet module for this.*/
- public ThreadProvider getThreadProvider(){
- return new ThreadProvider() {
- @Override public boolean isOnThread() {return true;}
- @Override public void sleep(long ms) {}
- @Override public void start(Runnable run) {}
- @Override public void stop() {}
- @Override public void notify(Object object) {}
- @Override public void wait(Object object) {}
- @Override public void switchContainer(EntityGroup group) {}
- };
- }
-}
+ public void showFileChooser(String text, String content, Consumer cons, boolean open, Predicate filetype){
+ }
+
+ /** Hide the app. Android only. */
+ public void hide(){
+ }
+
+ /** Forces the app into landscape mode. Currently Android only. */
+ public void beginForceLandscape(){
+ }
+
+ /** Stops forcing the app into landscape orientation. Currently Android only. */
+ public void endForceLandscape(){
+ }
+}
\ No newline at end of file
diff --git a/core/src/io/anuke/mindustry/core/Renderer.java b/core/src/io/anuke/mindustry/core/Renderer.java
index 214b5785c0..a6a5aa1363 100644
--- a/core/src/io/anuke/mindustry/core/Renderer.java
+++ b/core/src/io/anuke/mindustry/core/Renderer.java
@@ -1,593 +1,373 @@
package io.anuke.mindustry.core;
-import com.badlogic.gdx.Gdx;
-import com.badlogic.gdx.graphics.Color;
-import com.badlogic.gdx.graphics.Colors;
-import com.badlogic.gdx.graphics.Texture;
-import com.badlogic.gdx.graphics.Texture.TextureWrap;
-import com.badlogic.gdx.graphics.g2d.GlyphLayout;
-import com.badlogic.gdx.math.MathUtils;
-import com.badlogic.gdx.math.Rectangle;
-import com.badlogic.gdx.math.Vector2;
-import com.badlogic.gdx.utils.Array;
-import com.badlogic.gdx.utils.FloatArray;
-import com.badlogic.gdx.utils.Pools;
+import io.anuke.arc.ApplicationListener;
+import io.anuke.arc.Core;
+import io.anuke.arc.files.FileHandle;
+import io.anuke.arc.function.Consumer;
+import io.anuke.arc.function.Predicate;
+import io.anuke.arc.graphics.*;
+import io.anuke.arc.graphics.g2d.*;
+import io.anuke.arc.graphics.glutils.FrameBuffer;
+import io.anuke.arc.math.Mathf;
+import io.anuke.arc.math.geom.Rectangle;
+import io.anuke.arc.math.geom.Vector2;
+import io.anuke.arc.util.*;
+import io.anuke.arc.util.pooling.Pools;
+import io.anuke.mindustry.content.Fx;
import io.anuke.mindustry.core.GameState.State;
-import io.anuke.mindustry.entities.Player;
-import io.anuke.mindustry.entities.SyncEntity;
-import io.anuke.mindustry.entities.enemies.Enemy;
-import io.anuke.mindustry.game.SpawnPoint;
-import io.anuke.mindustry.graphics.BlockRenderer;
-import io.anuke.mindustry.graphics.Shaders;
-import io.anuke.mindustry.input.InputHandler;
-import io.anuke.mindustry.input.PlaceMode;
-import io.anuke.mindustry.ui.fragments.ToolFragment;
-import io.anuke.mindustry.world.BlockBar;
-import io.anuke.mindustry.world.Tile;
-import io.anuke.mindustry.world.blocks.Blocks;
-import io.anuke.mindustry.world.blocks.ProductionBlocks;
-import io.anuke.ucore.core.*;
-import io.anuke.ucore.entities.EffectEntity;
-import io.anuke.ucore.entities.Entities;
-import io.anuke.ucore.function.Callable;
-import io.anuke.ucore.graphics.*;
-import io.anuke.ucore.modules.RendererModule;
-import io.anuke.ucore.scene.ui.layout.Unit;
-import io.anuke.ucore.scene.utils.Cursors;
-import io.anuke.ucore.util.Angles;
-import io.anuke.ucore.util.Mathf;
-import io.anuke.ucore.util.Tmp;
+import io.anuke.mindustry.entities.*;
+import io.anuke.mindustry.entities.effect.GroundEffectEntity;
+import io.anuke.mindustry.entities.effect.GroundEffectEntity.GroundEffect;
+import io.anuke.mindustry.entities.impl.EffectEntity;
+import io.anuke.mindustry.entities.traits.*;
+import io.anuke.mindustry.entities.type.*;
+import io.anuke.mindustry.game.Team;
+import io.anuke.mindustry.graphics.*;
+import io.anuke.mindustry.world.blocks.defense.ForceProjector.ShieldEntity;
+import static io.anuke.arc.Core.*;
import static io.anuke.mindustry.Vars.*;
-import static io.anuke.ucore.core.Core.batch;
-import static io.anuke.ucore.core.Core.camera;
-public class Renderer extends RendererModule{
- private final static float shieldHitDuration = 18f;
-
- public Surface shadowSurface, shieldSurface, indicatorSurface;
-
- private int targetscale = baseCameraScale;
- private Texture background = new Texture("sprites/background.png");
- private FloatArray shieldHits = new FloatArray();
- private Array shieldDraws = new Array<>();
- private Rectangle rect = new Rectangle(), rect2 = new Rectangle();
- private BlockRenderer blocks = new BlockRenderer();
+public class Renderer implements ApplicationListener{
+ public final BlockRenderer blocks = new BlockRenderer();
+ public final MinimapRenderer minimap = new MinimapRenderer();
+ public final OverlayRenderer overlays = new OverlayRenderer();
+ public final Pixelator pixelator = new Pixelator();
- public Renderer() {
- Lines.setCircleVertices(14);
+ public FrameBuffer shieldBuffer = new FrameBuffer(2, 2);
+ private Color clearColor;
+ private float targetscale = io.anuke.arc.scene.ui.layout.Unit.dp.scl(4);
+ private float camerascale = targetscale;
+ private Rectangle rect = new Rectangle(), rect2 = new Rectangle();
+ private float shakeIntensity, shaketime;
- Core.cameraScale = baseCameraScale;
- Effects.setEffectProvider((name, color, x, y, rotation) -> {
- if(Settings.getBool("effects")){
- Rectangle view = rect.setSize(camera.viewportWidth, camera.viewportHeight)
- .setCenter(camera.position.x, camera.position.y);
- Rectangle pos = rect2.setSize(name.size).setCenter(x, y);
- if(view.overlaps(pos)){
- new EffectEntity(name, color, rotation).set(x, y).add(effectGroup);
- }
- }
- });
+ public Renderer(){
+ batch = new SpriteBatch(4096);
+ camera = new Camera();
+ Lines.setCircleVertices(20);
+ Shaders.init();
- Cursors.cursorScaling = 3;
- Cursors.outlineColor = Color.valueOf("444444");
- Cursors.arrow = Cursors.loadCursor("cursor");
- Cursors.hand = Cursors.loadCursor("hand");
- Cursors.ibeam = Cursors.loadCursor("ibar");
+ Effects.setScreenShakeProvider((intensity, duration) -> {
+ shakeIntensity = Math.max(intensity, shakeIntensity);
+ shaketime = Math.max(shaketime, duration);
+ });
- clearColor = Hue.lightness(0.4f);
- clearColor.a = 1f;
+ Effects.setEffectProvider((effect, color, x, y, rotation, data) -> {
+ if(effect == Fx.none) return;
+ if(Core.settings.getBool("effects")){
+ Rectangle view = camera.bounds(rect);
+ Rectangle pos = rect2.setSize(effect.size).setCenter(x, y);
- background.setWrap(TextureWrap.Repeat, TextureWrap.Repeat);
- }
+ if(view.overlaps(pos)){
- @Override
- public void init(){
- pixelate = Settings.getBool("pixelate");
- int scale = Settings.getBool("pixelate") ? Core.cameraScale : 1;
-
- shadowSurface = Graphics.createSurface(scale);
- shieldSurface = Graphics.createSurface(scale);
- indicatorSurface = Graphics.createSurface(scale);
- pixelSurface = Graphics.createSurface(scale);
- }
+ if(!(effect instanceof GroundEffect)){
+ EffectEntity entity = Pools.obtain(EffectEntity.class, EffectEntity::new);
+ entity.effect = effect;
+ entity.color.set(color);
+ entity.rotation = rotation;
+ entity.data = data;
+ entity.id++;
+ entity.set(x, y);
+ if(data instanceof Entity){
+ entity.setParent((Entity)data);
+ }
+ effectGroup.add(entity);
+ }else{
+ GroundEffectEntity entity = Pools.obtain(GroundEffectEntity.class, GroundEffectEntity::new);
+ entity.effect = effect;
+ entity.color.set(color);
+ entity.rotation = rotation;
+ entity.id++;
+ entity.data = data;
+ entity.set(x, y);
+ if(data instanceof Entity){
+ entity.setParent((Entity)data);
+ }
+ groundEffectGroup.add(entity);
+ }
+ }
+ }
+ });
- public void setPixelate(boolean pixelate){
- this.pixelate = pixelate;
- }
-
- @Override
- public void update(){
-
- if(Core.cameraScale != targetscale){
- float targetzoom = (float) Core.cameraScale / targetscale;
- camera.zoom = Mathf.lerpDelta(camera.zoom, targetzoom, 0.2f);
-
- if(Mathf.in(camera.zoom, targetzoom, 0.005f)){
- camera.zoom = 1f;
- Graphics.setCameraScale(targetscale);
- control.input().resetCursor();
- }
- }else{
- camera.zoom = Mathf.lerpDelta(camera.zoom, 1f, 0.2f);
- }
-
- if(state.is(State.menu)){
- clearScreen();
- }else{
- boolean smoothcam = Settings.getBool("smoothcam");
-
- if(world.getCore() == null || world.getCore().block() == ProductionBlocks.core){
- if(!smoothcam){
- setCamera(player.x, player.y);
- }else{
- smoothCamera(player.x, player.y, mobile ? 0.3f : 0.14f);
- }
- }else{
- smoothCamera(world.getCore().worldx(), world.getCore().worldy(), 0.4f);
- }
-
- if(Settings.getBool("pixelate"))
- limitCamera(4f, player.x, player.y);
-
- float prex = camera.position.x, prey = camera.position.y;
- updateShake(0.75f);
- float prevx = camera.position.x, prevy = camera.position.y;
- clampCamera(-tilesize / 2f, -tilesize / 2f + 1, world.width() * tilesize - tilesize / 2f, world.height() * tilesize - tilesize / 2f);
-
- float deltax = camera.position.x - prex, deltay = camera.position.y - prey;
-
- if(mobile){
- player.x += camera.position.x - prevx;
- player.y += camera.position.y - prevy;
- }
-
- float lastx = camera.position.x, lasty = camera.position.y;
-
- if(snapCamera && smoothcam && Settings.getBool("pixelate")){
- camera.position.set((int) camera.position.x, (int) camera.position.y, 0);
- }
-
- if(Gdx.graphics.getHeight() / Core.cameraScale % 2 == 1){
- camera.position.add(0, -0.5f, 0);
- }
-
- if(Gdx.graphics.getWidth() / Core.cameraScale % 2 == 1){
- camera.position.add(-0.5f, 0, 0);
- }
-
- draw();
-
- camera.position.set(lastx - deltax, lasty - deltay, 0);
-
- if(debug && !ui.chatfrag.chatOpen())
- record(); //this only does something if GdxGifRecorder is on the class path, which it usually isn't
- }
- }
-
- @Override
- public void draw(){
- camera.update();
-
- clearScreen(clearColor);
-
- batch.setProjectionMatrix(camera.combined);
-
- if(pixelate)
- Graphics.surface(pixelSurface, false);
- else
- batch.begin();
-
- //clears shield surface
- Graphics.surface(shieldSurface);
- Graphics.surface();
-
- drawPadding();
-
- blocks.drawFloor();
- blocks.processBlocks();
- blocks.drawBlocks(false);
-
- Graphics.shader(Shaders.outline, false);
- Entities.draw(enemyGroup);
- Entities.draw(playerGroup, p -> !p.isAndroid);
- Graphics.shader();
-
- Entities.draw(Entities.defaultGroup());
-
- blocks.drawBlocks(true);
-
- Graphics.shader(Shaders.outline, false);
- Entities.draw(playerGroup, p -> p.isAndroid);
- Graphics.shader();
-
- Entities.draw(bulletGroup);
- Entities.draw(effectGroup);
-
- drawShield();
-
- drawOverlay();
-
- if(Settings.getBool("indicators") && showUI){
- drawEnemyMarkers();
- }
-
- if(pixelate)
- Graphics.flushSurface();
-
- drawPlayerNames();
-
- batch.end();
- }
-
- @Override
- public void resize(int width, int height){
- super.resize(width, height);
- control.input().resetCursor();
- camera.position.set(player.x, player.y, 0);
- }
-
- @Override
- public void dispose() {
- background.dispose();
- }
-
- public void clearTiles(){
- blocks.clearTiles();
- }
-
- void drawPadding(){
- float vw = world.width() * tilesize;
- float cw = camera.viewportWidth * camera.zoom;
- float ch = camera.viewportHeight * camera.zoom;
- if(vw < cw){
- batch.draw(background,
- camera.position.x + vw/2,
- Mathf.round(camera.position.y - ch/2, tilesize),
- (cw - vw) /2,
- ch + tilesize,
- 0, 0,
- ((cw - vw) / 2 / tilesize), -ch / tilesize + 1);
-
- batch.draw(background,
- camera.position.x - vw/2,
- Mathf.round(camera.position.y - ch/2, tilesize),
- -(cw - vw) /2,
- ch + tilesize,
- 0, 0,
- -((cw - vw) / 2 / tilesize), -ch / tilesize + 1);
- }
- }
-
- void drawPlayerNames(){
- GlyphLayout layout = Pools.obtain(GlyphLayout.class);
-
- Draw.tscl(0.25f/2);
- for(Player player : playerGroup.all()){
- if(!player.isLocal && !player.isDead()){
- layout.setText(Core.font, player.name);
- Draw.color(0f, 0f, 0f, 0.3f);
- Draw.rect("blank", player.getDrawPosition().x, player.getDrawPosition().y + 8 - layout.height/2, layout.width + 2, layout.height + 2);
- Draw.color();
- Draw.tcolor(player.getColor());
- Draw.text(player.name, player.getDrawPosition().x, player.getDrawPosition().y + 8);
-
- if(player.isAdmin){
- Draw.color(player.getColor());
- float s = 3f;
- Draw.rect("icon-admin-small", player.getDrawPosition().x + layout.width/2f + 2 + 1, player.getDrawPosition().y + 7f, s, s);
- }
- Draw.reset();
- }
- }
- Pools.free(layout);
- Draw.tscl(fontscale);
+ clearColor = new Color(0f, 0f, 0f, 1f);
}
- void drawEnemyMarkers(){
- Graphics.surface(indicatorSurface);
- Draw.color(Color.RED);
+ @Override
+ public void update(){
+ //TODO hack, find source of this bug
+ Color.WHITE.set(1f, 1f, 1f, 1f);
- for(Enemy enemy : enemyGroup.all()) {
+ camerascale = Mathf.lerpDelta(camerascale, targetscale, 0.1f);
+ camera.width = graphics.getWidth() / camerascale;
+ camera.height = graphics.getHeight() / camerascale;
- if (rect.setSize(camera.viewportWidth, camera.viewportHeight).setCenter(camera.position.x, camera.position.y)
- .overlaps(enemy.hitbox.getRect(enemy.x, enemy.y))) {
- continue;
- }
+ if(state.is(State.menu)){
+ graphics.clear(Color.BLACK);
+ }else{
+ Vector2 position = Tmp.v3.set(player);
- float angle = Angles.angle(camera.position.x, camera.position.y, enemy.x, enemy.y);
- float tx = Angles.trnsx(angle, Unit.dp.scl(20f));
- float ty = Angles.trnsy(angle, Unit.dp.scl(20f));
- Draw.rect("enemyarrow", camera.position.x + tx, camera.position.y + ty, angle);
- }
+ if(player.isDead()){
+ TileEntity core = player.getClosestCore();
+ if(core != null && player.spawner == null){
+ camera.position.lerpDelta(core.x, core.y, 0.08f);
+ }else{
+ camera.position.lerpDelta(position, 0.08f);
+ }
+ }else if(!mobile){
+ camera.position.lerpDelta(position, 0.08f);
+ }
- Draw.color();
- Draw.alpha(0.4f);
- Graphics.flushSurface();
- Draw.color();
- }
+ updateShake(0.75f);
+ if(pixelator.enabled()){
+ pixelator.drawPixelate();
+ }else{
+ draw();
+ }
+ }
+ }
- void drawShield(){
- if(shieldGroup.size() == 0 && shieldDraws.size == 0) return;
-
- Graphics.surface(renderer.shieldSurface, false);
- Draw.color(Color.ROYAL);
- Entities.draw(shieldGroup);
- for(Callable c : shieldDraws){
- c.run();
- }
- Draw.reset();
- Graphics.surface();
-
- for(int i = 0; i < shieldHits.size / 3; i++){
- float time = shieldHits.get(i * 3 + 2);
+ @Override
+ public void dispose(){
+ minimap.dispose();
+ shieldBuffer.dispose();
+ blocks.dispose();
+ }
- time += Timers.delta() / shieldHitDuration;
- shieldHits.set(i * 3 + 2, time);
+ void updateShake(float scale){
+ if(shaketime > 0){
+ float intensity = shakeIntensity * (settings.getInt("screenshake", 4) / 4f) * scale;
+ camera.position.add(Mathf.range(intensity), Mathf.range(intensity));
+ shakeIntensity -= 0.25f * Time.delta();
+ shaketime -= Time.delta();
+ shakeIntensity = Mathf.clamp(shakeIntensity, 0f, 100f);
+ }else{
+ shakeIntensity = 0f;
+ }
+ }
- if(time >= 1f){
- shieldHits.removeRange(i * 3, i * 3 + 2);
- i--;
- }
- }
+ public void draw(){
+ camera.update();
- Texture texture = shieldSurface.texture();
- Shaders.shield.color.set(Color.SKY);
+ if(Float.isNaN(camera.position.x) || Float.isNaN(camera.position.y)){
+ camera.position.x = player.x;
+ camera.position.y = player.y;
+ }
- Tmp.tr2.setRegion(texture);
- Shaders.shield.region = Tmp.tr2;
- Shaders.shield.hits = shieldHits;
-
- if(Shaders.shield.isFallback){
- Draw.color(1f, 1f, 1f, 0.3f);
- Shaders.outline.color = Color.SKY;
- Shaders.outline.region = Tmp.tr2;
- }
+ graphics.clear(clearColor);
- Graphics.end();
- Graphics.shader(Shaders.shield.isFallback ? Shaders.outline : Shaders.shield);
- Graphics.setScreen();
+ if(!graphics.isHidden() && (Core.settings.getBool("animatedwater") || Core.settings.getBool("animatedshields")) && (shieldBuffer.getWidth() != graphics.getWidth() || shieldBuffer.getHeight() != graphics.getHeight())){
+ shieldBuffer.resize(graphics.getWidth(), graphics.getHeight());
+ }
- Core.batch.draw(texture, 0, Gdx.graphics.getHeight(), Gdx.graphics.getWidth(), -Gdx.graphics.getHeight());
+ Draw.proj(camera.projection());
- Graphics.shader();
- Graphics.end();
- Graphics.beginCam();
-
- Draw.color();
- shieldDraws.clear();
- }
+ blocks.floor.drawFloor();
- public BlockRenderer getBlocks() {
- return blocks;
- }
+ drawAndInterpolate(groundEffectGroup, e -> e instanceof BelowLiquidTrait);
+ drawAndInterpolate(puddleGroup);
+ drawAndInterpolate(groundEffectGroup, e -> !(e instanceof BelowLiquidTrait));
- public void addShieldHit(float x, float y){
- shieldHits.addAll(x, y, 0f);
- }
+ blocks.processBlocks();
- public void addShield(Callable call){
- shieldDraws.add(call);
- }
+ blocks.drawShadows();
+ Draw.color();
- void drawOverlay(){
+ blocks.floor.beginDraw();
+ blocks.floor.drawLayer(CacheLayer.walls);
+ blocks.floor.endDraw();
- //draw tutorial placement point
- if(world.getMap().name.equals("tutorial") && control.tutorial().showBlock()){
- int x = world.getCore().x + control.tutorial().getPlacePoint().x;
- int y = world.getCore().y + control.tutorial().getPlacePoint().y;
- int rot = control.tutorial().getPlaceRotation();
+ blocks.drawBlocks(Layer.block);
+ blocks.drawFog();
- Lines.stroke(1f);
- Draw.color(Color.YELLOW);
- Lines.square(x * tilesize, y * tilesize, tilesize / 2f + Mathf.sin(Timers.time(), 4f, 1f));
+ Draw.shader(Shaders.blockbuild, true);
+ blocks.drawBlocks(Layer.placement);
+ Draw.shader();
- Draw.color(Color.ORANGE);
- Lines.stroke(2f);
- if(rot != -1){
- Lines.lineAngle(x * tilesize, y * tilesize, rot * 90, 6);
- }
- Draw.reset();
- }
+ blocks.drawBlocks(Layer.overlay);
- //draw config selected block
- if(ui.configfrag.isShown()){
- Tile tile = ui.configfrag.getSelectedTile();
- Draw.color(Colors.get("accent"));
- Lines.stroke(1f);
- Lines.square(tile.drawx(), tile.drawy(),
- tile.block().width * tilesize / 2f + 1f);
- Draw.reset();
- }
-
- int tilex = control.input().getBlockX();
- int tiley = control.input().getBlockY();
-
- if(mobile){
- Vector2 vec = Graphics.world(Gdx.input.getX(0), Gdx.input.getY(0));
- tilex = Mathf.scl2(vec.x, tilesize);
- tiley = Mathf.scl2(vec.y, tilesize);
- }
+ drawGroundShadows();
- InputHandler input = control.input();
+ drawAllTeams(false);
- //draw placement box
- if((input.recipe != null && state.inventory.hasItems(input.recipe.requirements) && (!ui.hasMouse() || mobile)
- && control.input().drawPlace())){
+ blocks.skipLayer(Layer.turret);
+ blocks.drawBlocks(Layer.laser);
- input.placeMode.draw(control.input().getBlockX(), control.input().getBlockY(),
- control.input().getBlockEndX(), control.input().getBlockEndY());
+ drawFlyerShadows();
- Lines.stroke(1f);
- Draw.color(Color.SCARLET);
- for(SpawnPoint spawn : world.getSpawns()){
- Lines.dashCircle(spawn.start.worldx(), spawn.start.worldy(), enemyspawnspace);
- }
+ drawAllTeams(true);
- if(world.getCore() != null) {
- Draw.color(Color.LIME);
- Lines.poly(world.getSpawnX(), world.getSpawnY(), 4, 6f, Timers.time() * 2f);
- }
-
- if(input.breakMode == PlaceMode.holdDelete)
- input.breakMode.draw(tilex, tiley, 0, 0);
-
- }else if(input.breakMode.delete && control.input().drawPlace()
- && (input.recipe == null || !state.inventory.hasItems(input.recipe.requirements))
- && (input.placeMode.delete || input.breakMode.both || !mobile)){
+ drawAndInterpolate(bulletGroup);
+ drawAndInterpolate(effectGroup);
- if(input.breakMode == PlaceMode.holdDelete)
- input.breakMode.draw(tilex, tiley, 0, 0);
- else
- input.breakMode.draw(control.input().getBlockX(), control.input().getBlockY(),
- control.input().getBlockEndX(), control.input().getBlockEndY());
- }
+ overlays.drawBottom();
+ drawAndInterpolate(playerGroup, p -> true, Player::drawBuildRequests);
- if(ui.toolfrag.confirming){
- ToolFragment t = ui.toolfrag;
- PlaceMode.areaDelete.draw(t.px, t.py, t.px2, t.py2);
- }
-
- Draw.reset();
+ if(Entities.countInBounds(shieldGroup) > 0){
+ if(settings.getBool("animatedshields")){
+ Draw.flush();
+ shieldBuffer.begin();
+ graphics.clear(Color.CLEAR);
+ Entities.draw(shieldGroup);
+ Entities.draw(shieldGroup, shield -> true, shield -> ((ShieldEntity)shield).drawOver());
+ Draw.flush();
+ shieldBuffer.end();
+ Draw.shader(Shaders.shield);
+ Draw.color(Pal.accent);
+ Draw.rect(Draw.wrap(shieldBuffer.getTexture()), camera.position.x, camera.position.y, camera.width, -camera.height);
+ Draw.color();
+ Draw.shader();
+ }else{
+ Entities.draw(shieldGroup, shield -> true, shield -> ((ShieldEntity)shield).drawSimple());
+ }
+ }
- //draw selected block bars and info
- if(input.recipe == null && !ui.hasMouse()){
- Tile tile = world.tileWorld(Graphics.mouseWorld().x, Graphics.mouseWorld().y);
+ overlays.drawTop();
- if(tile != null && tile.block() != Blocks.air){
- Tile target = tile;
- if(tile.isLinked())
- target = tile.getLinked();
+ drawAndInterpolate(playerGroup, p -> !p.isDead() && !p.isLocal, Player::drawName);
- if(showBlockDebug && target.entity != null){
- Draw.color(Color.RED);
- Lines.crect(target.drawx(), target.drawy(), target.block().width * tilesize, target.block().height * tilesize);
- Vector2 v = new Vector2();
+ Draw.color();
+ Draw.flush();
+ }
+ private void drawGroundShadows(){
+ Draw.color(0, 0, 0, 0.4f);
+ float rad = 1.6f;
+ Consumer draw = u -> {
+ float size = Math.max(u.getIconRegion().getWidth(), u.getIconRegion().getHeight()) * Draw.scl;
+ Draw.rect("circle-shadow", u.x, u.y, size * rad, size * rad);
+ };
- Draw.tcolor(Color.YELLOW);
- Draw.tscl(0.25f);
- Array