diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index b26968cd34..01d804c3fd 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -16,7 +16,7 @@ android:theme="@style/GdxTheme" > diff --git a/build.gradle b/build.gradle index 2ba9f9dfad..4b4ab3045a 100644 --- a/build.gradle +++ b/build.gradle @@ -20,12 +20,12 @@ allprojects { version = 'release' ext { - versionNumber = '3.3' + versionNumber = '3.4' versionType = 'release' appName = 'Mindustry' gdxVersion = '1.9.8' aiVersion = '1.8.1' - uCoreVersion = '23e8c1c' + uCoreVersion = '0a15aeb' getVersionString = { String buildVersion = getBuildVersion() diff --git a/core/assets-raw/sprites/ui/icons/icon-admin-small.png b/core/assets-raw/sprites/ui/icons/icon-admin-small.png new file mode 100644 index 0000000000..c5cf9ef819 Binary files /dev/null and b/core/assets-raw/sprites/ui/icons/icon-admin-small.png differ diff --git a/core/assets-raw/sprites/ui/icons/icon-admin.png b/core/assets-raw/sprites/ui/icons/icon-admin.png new file mode 100644 index 0000000000..3d3811462a Binary files /dev/null and b/core/assets-raw/sprites/ui/icons/icon-admin.png differ diff --git a/core/assets-raw/sprites/ui/icons/icon-ban.png b/core/assets-raw/sprites/ui/icons/icon-ban.png new file mode 100644 index 0000000000..f8df32c20c Binary files /dev/null and b/core/assets-raw/sprites/ui/icons/icon-ban.png differ diff --git a/core/assets-raw/sprites/ui/icons/icon-zoom-small.png b/core/assets-raw/sprites/ui/icons/icon-zoom-small.png new file mode 100644 index 0000000000..35c117cda3 Binary files /dev/null and b/core/assets-raw/sprites/ui/icons/icon-zoom-small.png differ diff --git a/core/assets/bundles/bundle.properties b/core/assets/bundles/bundle.properties index c466b43694..802247c4d2 100644 --- a/core/assets/bundles/bundle.properties +++ b/core/assets/bundles/bundle.properties @@ -25,6 +25,7 @@ text.server.kicked.kick=You have been kicked from the server! text.server.kicked.invalidPassword=Invalid password! text.server.kicked.clientOutdated=Outdated client! Update your game! text.server.kicked.serverOutdated=Outdated server! Ask the host to update! +text.server.kicked.banned=You are banned on this server. text.server.connected={0} has joined. text.server.disconnected={0} has disconnected. text.nohost=Can't host server on a custom map! @@ -37,10 +38,27 @@ text.server.refreshing=Refreshing server text.hosts.none=[lightgray]No LAN games found! text.host.invalid=[scarlet]Can't connect to host. text.server.friendlyfire=Friendly Fire +text.trace=Trace Player +text.trace.playername=Player name: [accent]{0} +text.trace.ip=IP: [accent]{0} +text.trace.modclient=Custom Client: [accent]{0} +text.trace.totalblocksbroken=Total blocks broken: [accent]{0} +text.trace.structureblocksbroken=Structure blocks broken: [accent]{0} +text.trace.lastblockbroken=Last block broken: [accent]{0} +text.trace.totalblocksplaced=Total blocks placed: [accent]{0} +text.trace.lastblockplaced=Last block placed: [accent]{0} +text.server.bans=Bans +text.server.bans.none=No banned players found! +text.server.admins=Admins +text.server.admins.none=No admins found! text.server.add=Add Server text.server.delete=Are you sure you want to delete this server? text.server.hostname=Host: {0} text.server.edit=Edit Server +text.confirmban=Are you sure you want to ban this player? +text.confirmunban=Are you sure you want to unban this player? +text.confirmadmin=Are you sure you want to make this player an admin? +text.confirmunadmin=Are you sure you want to remove admin status from this player? text.joingame.byip=Join by IP... text.joingame.title=Join Game text.joingame.ip=IP: diff --git a/core/assets/bundles/bundle_de.properties b/core/assets/bundles/bundle_de.properties new file mode 100644 index 0000000000..be8fa12cf5 --- /dev/null +++ b/core/assets/bundles/bundle_de.properties @@ -0,0 +1,479 @@ +text.about = Erstellt von [ROYAL] Anuken. [] \nUrsprünglich ein Eintrag im [orange] GDL [] MM Jam.\n\nCredits: \n- SFX gemacht mit [yellow] bfxr [] - Musik gemacht von [green] RoccoW [] / gefunden auf [lime] FreeMusicArchive.org [] \n\nBesonderer Dank geht an: \n- [coral] MitchellFJN []: Umfangreicher Spieletest und Feedback \n- [sky] Luxray5474 []: Wiki-Arbeit, Code-Beiträge \n- Alle Beta-Tester auf itch.io und Google Play\n +text.discord = Trete dem Mindustry Discord bei! +text.gameover = Der Kern wurde zerstört. +text.highscore = [YELLOW] Neuer Highscore! +text.lasted = Du hast bis zur folgenden Welle überlebt +text.level.highscore = High Score: [accent] {0} +text.level.delete.title = Löschen bestätigen +text.level.delete = Bist du sicher, dass du die Karte \"[orange] {0}\" löschen möchtest? +text.level.select = Level Auswahl +text.level.mode = Spielmodus: +text.savegame = Spiel speichern +text.loadgame = Spiel laden +text.joingame = Spiel beitreten +text.quit = Verlassen +text.about.button = Info +text.name = Name: +text.public = Öffentlich +text.players = {0} Spieler online +text.players.single = {0} Spieler online +text.server.mismatch = Paketfehler: Mögliche Client / Server-Version stimmt nicht überein. Stell sicher, dass du und der Host die neueste Version von Mindustry haben! +.server.closing = [accent] Server wird geschlossen... +text.server.kicked.kick = Du wurdest vom Server gekickt! +text.server.kicked.invalidPassword = Falsches Passwort. +text.server.connected = {0} ist beigetreten +text.server.disconnected = {0} hat die Verbindung getrennt. +text.nohost = Server kann nicht auf einer benutzerdefinierten Karte gehostet werden! +text.hostserver = Server hosten +text.host = Host +text.hosting = [accent] Server wird geöffnet... +text.hosts.refresh = Aktualisieren +text.hosts.discovering = Suche nach LAN-Spielen +text.server.refreshing = Server wird aktualisiert +text.hosts.none = [lightgray] Keine LAN Spiele gefunden! +text.host.invalid = [scarlet] Kann keine Verbindung zum Host herstellen. +text.server.add = Server hinzufügen +text.server.delete = Bist du dir sicher das du diesen Server löschen möchtest? +text.server.hostname = Host: {0} +text.server.edit = Server bearbeiten +text.joingame.byip = Über IP beitreten ... +text.joingame.title = Spiel beitreten +text.joingame.ip = IP: +text.disconnect = Verbindung unterbrochen. +text.connecting = [accent] Verbindet... +text.connecting.data = [accent] Weltdaten werden geladen... +text.connectfail = [crimson] Verbindung zum Server konnte nicht hergestellt werden: [orange]{0} +text.server.port = Port: +text.server.invalidport = Falscher Port! +text.server.error = [crimson] Fehler beim Hosten des Servers: [orange] {0} +text.tutorial.back = < Zurück +text.tutorial.next = Weiter > +text.save.new = Neuer Spielstand +text.save.overwrite = Möchten du diesen Spielstand wirklich überschreiben? +text.overwrite = Überschreiben +text.save.none = Keine Spielstände gefunden! +text.saveload = [accent] Speichern ... +text.savefail = Fehler beim Speichern des Spiels! +text.save.delete.confirm = Möchtest du diesen Spielstand wirklich löschen? +text.save.delete = Löschen +text.save.export = Spielstand exportieren +text.save.import.invalid = [orange] Dieser Spielstand ist ungültig! +text.save.import.fail = [crimson] Spielstand konnte nicht importiert werden: [orange] {0} +text.save.export.fail = [crimson] Spielstand konnte nicht exportiert werden: [orange] {0} +text.save.import = Spielstand importieren +text.save.newslot = Name speichern: +text.save.rename = Umbenennen +text.save.rename.text = Neuer Name +text.selectslot = Wähle einen Spielstand +text.slot = [accent] Platz {0} +text.save.corrupted = [orange] Datei beschädigt oder ungültig! +text.empty = +text.on = An +text.off = Aus +text.save.autosave = Automatisches Speichern: {0} +text.save.map = Karte: {0} +text.save.wave = Welle: {0} +text.save.date = Zuletzt gespeichert: {0} +text.confirm = Bestätigen +text.delete = Löschen +text.ok = OK +text.open = Öffnen +text.cancel = Abbruch +text.openlink = Link öffnen +text.back = Zurück +text.quit.confirm = Willst du wirklich aufhören? +text.loading = [accent] Wird geladen ... +text.wave = [orange] Welle {0} +text.wave.waiting = Welle in {0} +text.waiting = Warten... +text.enemies = {0} Feinde +text.enemies.single = {0} Feind +text.loadimage = Bild laden +text.saveimage = Bild speichern +text.editor.badsize = [orange]Ungültige Bildabmessungen! [] Gültige Kartenabmessungen: {0} +text.editor.errorimageload = Fehler beim Laden des Bildes: [orange] {0} +text.editor.errorimagesave = Fehler beim Speichern des Bildes: [orange] {0} +text.editor.generate = Generieren +text.editor.resize = Grösse\nanpassen +text.editor.loadmap = Karte\nladen +text.editor.savemap = Karte\nspeichern +text.editor.loadimage = Bild\nladen +text.editor.saveimage = Bild\nspeichern +text.editor.unsaved = [crimson] Du hast Änderungen nicht gespeichert [] Möchtest du wirklich aufhören? +text.editor.brushsize = Pinselgrösse: {0} +text.editor.noplayerspawn = Diese Karte hat keinen Spielerspawnpunkt! +text.editor.manyplayerspawns = Maps können nicht mehr als einen Spawnpunkt des Spielers haben! +text.editor.manyenemyspawns = Kann nicht mehr als {0} feindliche Spawnpunkte haben! +text.editor.resizemap = Grösse der Karte ändern +text.editor.resizebig = [crimson] Warnung! [] Karten, die grösser als 256 Einheiten sind, können ruckeln und instabil sein. +text.editor.mapname = Map Name +text.editor.overwrite = [accent] Warnung! Dies überschreibt eine vorhandene Map. +text.editor.failoverwrite = [crimson] Die Standardkarte kann nicht überschrieben werden! +text.editor.selectmap = Wähle eine Map zum Laden: +text.width = Breite: +text.height = Höhe: +text.randomize = Zufällig +text.apply = Anwenden +text.update = Aktualisieren +text.menu = Menü +text.play = Spielen +text.load = Laden +text.save = Speichern +text.settings = Einstellungen +text.tutorial = Tutorial +text.editor = Bearbeiter +text.mapeditor = Karten Bearbeiter +text.donate = Spenden +text.settings.reset = Auf Standard zurücksetzen +text.settings.controls = Steuerung +text.settings.game = Spiel +text.settings.sound = Audio +text.settings.graphics = Grafiken +text.upgrades = Verbesserungen +text.purchased = [LIME] Erstellt! +text.weapons = Waffen +text.paused = Pausiert +text.respawn = Respawn in +text.error.title = [crimson] Ein Fehler ist aufgetreten +text.error.crashmessage = [SCARLET] Es ist ein unerwarteter Fehler aufgetreten, der einen Absturz verursacht hätte. [] Bitte geben Sie die genauen Umstände an, unter denen dieser Fehler passiert ist, für den Entwickler: [ORANGE] anukendev@gmail.com [] +text.error.crashtitle = EIn Fehler ist aufgetreten! +text.mode.break = Zerstörungsmodus: {0} +text.mode.place = Platzierungsmodus: {0} +placemode.hold.name = Zeile +placemode.areadelete.name = Gebiet +placemode.touchdelete.name = berühren +placemode.holddelete.name = halten +placemode.none.name = keine +placemode.touch.name = berühren +placemode.cursor.name = Mauszeiger +text.blocks.extrainfo = [accent] Extra Blockinfo: +text.blocks.blockinfo = Blockinfo: +text.blocks.powercapacity = Energiekapazität +text.blocks.powershot = Energie / Schuss +text.blocks.powersecond = Energie / Sekunde +text.blocks.powerdraindamage = Energieabnahme / Schaden +text.blocks.shieldradius = Schildradius +text.blocks.itemspeedsecond = Gegenstands Geschwindigkeit / Sekunde +text.blocks.range = Reichweite +text.blocks.size = Grösse +text.blocks.powerliquid = Energie / Flüssigkeit +text.blocks.maxliquidsecond = Max Flüssigkeit / Sekunde +text.blocks.liquidcapacity = Flüssigkeitskapazität +text.blocks.liquidsecond = Flüssigkeit / Sekunde +text.blocks.damageshot = Schaden / Schuss +text.blocks.ammocapacity = Munitionskapazität +text.blocks.ammo = Munition +text.blocks.ammoitem = Munition / Gegenstand +text.blocks.maxitemssecond = Max Gegenstände / Sekunde +text.blocks.powerrange = Energiereichweite +text.blocks.lasertilerange = Laser Reichweite +text.blocks.capacity = Kapazität +text.blocks.itemcapacity = Gegenstand Kapazität +text.blocks.maxpowergenerationsecond = Max Energieerzeugung / Sekunde +text.blocks.powergenerationsecond = Energieerzeugung / Sekunde +text.blocks.generationsecondsitem = Generation Sekunden / Gegenstand +text.blocks.input = Eingabe +text.blocks.inputliquid = Flüssigkeiten Eingabe +text.blocks.inputitem = Eingabe Gegenstand +text.blocks.output = Ausgabe +text.blocks.secondsitem = Sekunden / Item +text.blocks.maxpowertransfersecond = Max Energieübertragung / Sekunde +text.blocks.explosive = Hochexplosiv! +text.blocks.repairssecond = Reparaturen / Sekunde +text.blocks.health = Lebenspunkte +text.blocks.inaccuracy = Ungenauigkeit +text.blocks.shots = Schüsse +text.blocks.shotssecond = Schüsse / Sekunde +text.blocks.fuel = Treibstoff +text.blocks.fuelduration = Treibstoffdauer +text.blocks.maxoutputsecond = Max Ausgabe / Sekunde +text.blocks.inputcapacity = Eingabekapazität +text.blocks.outputcapacity = Ausgabekapazität +text.blocks.poweritem = Energie / Gegenstand +text.placemode = Platzierungsmodus +text.breakmode = Zerstörungsmodus +text.health = Lebenspunkte +setting.difficulty.easy = Leicht +setting.difficulty.normal = Normal +setting.difficulty.hard = Schwer +setting.difficulty.insane = Unmöglich +setting.difficulty.purge = Auslöschung +setting.difficulty.name = Schwierigkeit +setting.screenshake.name = Bildschirm wackeln +setting.smoothcam.name = Glatte Kamera +setting.indicators.name = Feind Indikatoren +setting.effects.name = Effekte anzeigen +setting.sensitivity.name = Kontroller Empfindlichkeit +setting.saveinterval.name = Autosave Häufigkeit +setting.seconds = {0} Sekunden +setting.fps.name = Zeige FPS +setting.vsync.name = VSync +setting.lasers.name = Zeige Energielaser +setting.healthbars.name = Zeige Objekt Lebensbalken +setting.pixelate.name = Pixel Bildschirm +setting.musicvol.name = Musiklautstärke +setting.mutemusic.name = Musik stummschalten +setting.sfxvol.name = Audioeffekte Lautstärke +setting.mutesound.name = Audioeffekte stummschalten +map.maze.name = Labyrinth +map.fortress.name = Festung +map.sinkhole.name = Sinkloch +map.caves.name = Höhlen +map.volcano.name = Vulkan +map.caldera.name = Lavakessel +map.scorch.name = Flammen +map.desert.name = Wüste +map.island.name = Insel +map.grassland.name = Grasland +map.tundra.name = Kältesteppe +map.spiral.name = Spirale +map.tutorial.name = Tutorial +tutorial.intro.text = [gelb] Willkommen zum Tutorial [] Um zu beginnen, drücke 'weiter'. +tutorial.moveDesktop.text = Verwende zum Verschieben die Tasten [orange] [[WASD] []. Halte [orange] Shift [] gedrückt, um zu erhöhen. Halte [orange] CTRL/STRG [] gedrückt, während du das [orange] Scrollrad [] zum Vergrössern oder Verkleinern verwendest. +tutorial.shoot.text = Ziele mit der Maus, halte die [orange] linke Maustaste [] gedrückt, um zu schiessen. Versuche es mit dem [gelben] Ziel []. +tutorial.moveAndroid.text = Um die Ansicht zu verschieben, ziehe einen Finger über den Bildschirm. Drücke und ziehe, um zu vergrössern oder zu verkleinern. +tutorial.placeSelect.text = Versuche, ein [gelbes] Förderband [] aus dem Blockmenü unten rechts auszuwählen. +tutorial.placeConveyorDesktop.text = Verwende das [orange] [[scrollwheel] [], um das Förderband nach vorne zu bewegen [orange] vorwärts [], und platziere es dann an der [gelben] markierten Stelle [] mit [orange] [[linke Maustaste] []. +tutorial.placeConveyorAndroid.text = Verwende die [orange] [[rotieren-Taste] [], um das Förderband nach vorne [orange] zu drehen [], ziehe es mit einem Finger in Position und platziere es dann an der [gelben] markierten Stelle [] mit der [Orange] [[Häkchen][]. +tutorial.placeConveyorAndroidInfo.text = Alternativ kannst du das Fadenkreuzsymbol unten links drücken, um zum [orange] [[touch mode] [] zu wechseln, und Blöcke durch Tippen auf den Bildschirm platzieren. Im Touch-Modus können Blöcke mit dem Pfeil unten links gedreht werden. Drücke [gelb] neben [], um es auszuprobieren. +tutorial.placeDrill.text = Wähle nun einen [gelben] Steinbohrer [] an der markierten Stelle aus und platziere ihn. +tutorial.blockInfo.text = Wenn du mehr über einen Block erfahren möchtest, tippe oben rechts auf das [orange] Fragezeichen [], um dessen Beschreibung zu lesen. +tutorial.deselectDesktop.text = Du kannst einen Block mit [Orange] [[Rechte Maustaste] [] abwählen. +tutorial.deselectAndroid.text = Du kannst einen Block abwählen, indem du die [orange] X [] -Taste drücken. +tutorial.drillPlaced.text = Der Bohrer erzeugt nun [gelben] Stein, [] gib den Stein nun auf das Förderband aus und bewege ihn dann in den [gelben] Kern []. +tutorial.drillInfo.text = Verschiedene Erze benötigen unterschiedliche Bohrer. Stein erfordert Steinbohrer, Eisen erfordert Eisenbohrer usw. +tutorial.drillPlaced2.text = Wenn du Gegenstände in den Kern verschiebst, steckst du sie in dein [gelbes] Inventar [] oben links. Das Platzieren von Blöcken verwendet Gegenstände aus deinem Inventar. +tutorial.moreDrills.text = Du kannst so viele Bohrer und Förderer miteinander verbinden wie du lust hast. +tutorial.deleteBlock.text = Du kannst Blöcke löschen, indem du mit der [orange] rechte Maustaste [] auf dem Block klickst, den du löschen möchtest. Versuche, dieses Förderband zu löschen. +tutorial.deleteBlockAndroid.text = Du kannst Blöcke löschen, indem du [orange] das Fadenkreuz [] im [orange] Pausenmodusmenü [] links unten auswählst und auf einen Block tippst. Versuche, dieses Fliessband zu löschen. +tutorial.placeTurret.text = Wähle nun einen [gelben] Turm [] an der [gelben] markierten Stelle [] und platziere ihn. +tutorial.placedTurretAmmo.text = Dieser Turm nimmt nun [gelbe] Munition [] vom Förderband an. Du kannst sehen, wie viel Munition es hat, indem du darüber schweben und den [grünen] grünen Balken [] prüfen. +tutorial.turretExplanation.text = Geschütze schiessen automatisch auf den nächsten Feind in Reichweite, solange sie genug Munition haben. +tutorial.waves.text = Jede [yellow] 60 [] Sekunden wird eine Welle von [coral] Feinden [] an einem bestimmten Orten erscheinen und versuchen, den Kern zu zerstören. +tutorial.coreDestruction.text = Dein Ziel ist es, den Kern [yellow] zu verteidigen. Wenn der Kern zerstört wird, verlierst du [coral] das Spiel []. +tutorial.pausingDesktop.text = Wenn du jemals eine Pause machen möchtest, drücke die [orange] Pause-Taste [] oben links oder [orange]space[], um das Spiel anzuhalten. Du kannst auch Blöcke immer noch auswählen und platzieren, während du das Spiel pausiert ist, aber Sie können sich nicht bewegen oder schiessen. +tutorial.pausingAndroid.text = Wenn du jemals eine Pause machen willst, drück einfach die [orange] Pause-Taste [] oben links, um das Spiel anzuhalten. Sie können immer noch Sachen auswählen und Blöcke während der Pause platzieren. +tutorial.purchaseWeapons.text = Du kannst neue [yellow] Waffen [] für deinen Mech kaufen, indem du das Verbesserungs-Menü unten links öffnest. +tutorial.switchWeapons.text = Schalte Waffen, indem du entweder auf das Symbol unten links klickst oder Nummern verwendest [orange] [[1-9] []. +tutorial.spawnWave.text = Hier kommt die erste Welle. Zerstöre sie. +tutorial.pumpDesc.text = In späteren Wellen müsst du möglicherweise [yellow] Pumpen [] verwenden, um Flüssigkeiten für Generatoren oder Extraktoren zu verteilen. +tutorial.pumpPlace.text = Pumpen arbeiten ähnlich wie Bohrer, ausser dass sie anstelle von Gegenständen Flüssigkeiten produzieren. Versuch mal, eine Pumpe auf das [yellow] gekennzeichnete Öl [] zu setzen. +tutorial.conduitUse.text = Stellen Sie nun eine [orange] Leitungsrohr [] von der Pumpe weg. +tutorial.conduitUse2.text = Und noch ein paar mehr ... +tutorial.conduitUse3.text = Und noch ein paar mehr ... +tutorial.generator.text = Stellen Sie nun einen [orange] Verbrennungsgenerator [] Block am Ende des Leitungsrohres auf. +tutorial.generatorExplain.text = Dieser Generator erzeugt nun [yellow] Energie [] aus dem Öl. +tutorial.lasers.text = Die Energie wird mit [yellow] Energielasern [] verteilt. Drehe und platziere einen hier. +tutorial.laserExplain.text = Der Generator wird nun Energie in den Laserblock bewegen. Ein [yellow] undurchsichtiger [] Strahl bedeutet, dass er gerade Leistung überträgt, und ein [yellow] transparenter [] Strahl bedeutet, dass dies nicht der Fall ist. +tutorial.laserMore.text = Du kannst überprüfen, wie viel Energie ein Block hat, indem du darüber schweben und den [yellow] gelben Balken [] oben prüfen. +tutorial.healingTurret.text = Dieser Laser kann verwendet werden, um einen [lime] -Reparaturgeschütz [] anzutreiben. Platziere einen hier. +tutorial.healingTurretExplain.text = Solange er Kraft hat, repariert dieser Turm in der Nähe Blöcke. [] Wenn du spielst, stelle sicher, dass du so schnell wie möglich einen in deiner Basis bekommst! +tutorial.smeltery.text = Viele Blöcke benötigen [orange] Stahl [], um eine [orange] Schmelzer [] herzustellen. Platziere einen hier. +tutorial.smelterySetup.text = Diese Schmelzer wird nun [orange] Stahl [] aus dem Eingangs-Eisen produzieren, wobei Kohle als Brennstoff verwendet wird. +tutorial.end.text = Und damit ist das Tutorial abgeschlossen! Viel Glück! +keybind.move_x.name = bewege_x +keybind.move_y.name = bewege_y +keybind.select.name = wählen +keybind.break.name = Unterbrechung +keybind.shoot.name = Schiess +keybind.zoom_hold.name = zoomen_halten +keybind.zoom.name = zoomen +keybind.menu.name = Menü +keybind.pause.name = Pause +keybind.dash.name = Bindestrich +keybind.rotate_alt.name = drehen_alt +keybind.rotate.name = Drehen +keybind.weapon_1.name = Waffe_1 +keybind.weapon_2.name = Waffe_2 +keybind.weapon_3.name = Waffe_3 +keybind.weapon_4.name = Waffe_4 +keybind.weapon_5.name = Waffe_5 +keybind.weapon_6.name = Waffe_6 +mode.waves.name = Wellen +mode.sandbox.name = Sandkasten +mode.freebuild.name = Freier Bau +upgrade.standard.name = Standard +upgrade.standard.description = Der Standardmech. +upgrade.blaster.name = Blaster +upgrade.blaster.description = Schiesst eine langsame, schwache Kugel. +upgrade.triblaster.name = Dreifach-Blaster +upgrade.triblaster.description = Schiesst 3 Kugeln in einer Ausbreitung. +upgrade.clustergun.name = Klumpen-Waffe +upgrade.clustergun.description = Schiesst eine ungenaue Verbreitung von explosiven Granaten. +upgrade.beam.name = Strahlkanone +upgrade.beam.description = Schiesst einen weitreichenden durchdringenden Laserstrahl. +upgrade.vulcan.name = Vulkan +upgrade.vulcan.description = Schiesst eine Flut von schnellen Kugeln. +upgrade.shockgun.name = Schock-Waffe +upgrade.shockgun.description = Erschiesst eine verheerende Explosion von geladenen Granatsplittern. +item.stone.name = Stein +item.iron.name = Eisen +item.coal.name = Kohle +item.steel.name = Stahl +item.titanium.name = Titan +item.dirium.name = Dirium +item.uranium.name = Uran +item.sand.name = Sand +liquid.water.name = Wasser +liquid.plasma.name = Plasma +liquid.lava.name = Lava +liquid.oil.name = Öl +block.weaponfactory.name = Waffenfabrik +block.air.name = Luft +block.blockpart.name = Blockteil +block.deepwater.name = tiefes Wasser +block.water.name = Wasser +block.lava.name = Lava +block.oil.name = Öl +block.stone.name = Stein +block.blackstone.name = schwarzer Stein +block.iron.name = Eisen +block.coal.name = Kohle +block.titanium.name = Titan +block.uranium.name = Uran +block.dirt.name = Erde +block.sand.name = Sand +block.ice.name = Eis +block.snow.name = Schnee +block.grass.name = Gras +block.sandblock.name = Sandstein +block.snowblock.name = Schneeblock +block.stoneblock.name = Steinblock +block.blackstoneblock.name = Schwarzer Stein +block.grassblock.name = Grasblock +block.mossblock.name = Moosblock +block.shrub.name = Busch +block.rock.name = Felsen +block.icerock.name = Eisfelsen +block.blackrock.name = Schwarzer Felsen +block.dirtblock.name = Erdblock +block.stonewall.name = Steinwand +block.stonewall.fulldescription = Ein billiger Verteidigungsblock. Nützlich zum Schutz des Kerns und der Geschütztürme in den ersten Wellen. +block.ironwall.name = Eisenwand +block.ironwall.fulldescription = Ein grundlegender Verteidigungsblock. Bietet Schutz vor Feinden. +block.steelwall.name = Stahlwand +block.steelwall.fulldescription = Ein Standard-Verteidigungsblock. Bietet angemessen Schutz vor Feinden. +block.titaniumwall.name = Titanwand +block.titaniumwall.fulldescription = Eine starke Abwehrblockade. Bietet Schutz vor Feinden. +block.duriumwall.name = Diriumwand +block.duriumwall.fulldescription = Eine sehr starke Abwehrblockade. Bietet guten Schutz vor Feinden. +block.compositewall.name = Verbundende Wand +block.steelwall-large.name = grosse Stahlwand +block.steelwall-large.fulldescription = Ein Standard-Verteidigungsblock. Mehrere Blöcke gross. +block.titaniumwall-large.name = grosse Titanwand +block.titaniumwall-large.fulldescription = Eine starke Abwehrblockade. Mehrere Blöcke gross. +block.duriumwall-large.name = grosse Diriumwand +block.duriumwall-large.fulldescription = Eine sehr starke Abwehrblockade. Mehrere Blöcke gross. +block.titaniumshieldwall.name = geschützte Wand +block.titaniumshieldwall.fulldescription = Ein starker Abwehrblock mit einem extra eingebauten Schild. Benötigt Energie. Verwendet Energie, um feindliche Kugeln zu absorbieren. Es wird empfohlen, Verstärker zu verwenden, um diesem Block Energie zuzuführen. +block.repairturret.name = Reparaturgeschütz +block.repairturret.fulldescription = Repariert beschädigte Blöcke in der Nähe in einem langsamen Tempo. Nutzt geringe Mengen an Energie. +block.megarepairturret.name = Reparaturgeschütz II +block.megarepairturret.fulldescription = Repariert in der Nähe beschädigte Blöcke in Reichweite zu einem vernünftigen Preis. Verwendet Energie. +block.shieldgenerator.name = Schildgenerator +block.shieldgenerator.fulldescription = Ein fortgeschrittener Verteidigungsblock. Schützt alle Blöcke in einem Radius vor Angriffen. Bei keinem Betrieb langsamer Strom verbrauch, verliert bei Kugelkontakt jedoch schnell Energie. +block.door.name = Tür +block.door.fulldescription = Ein Block, der durch Antippen geöffnet und geschlossen werden kann. +block.door-large.name = grosse Tür +block.door-large.fulldescription = Ein Block der mehrere Felder gross ist und der durch Antippen geöffnet und geschlossen werden kann. +block.conduit.name = Leitungsrohr +block.conduit.fulldescription = Grundlegender Flüssigkeitstransportblock. Funktioniert wie ein Förderband, aber mit Flüssigkeiten. Am besten mit Pumpen oder anderen Leitungen verwenden. Kann als Brücke für Gegner und Spieler verwendet werden. +block.pulseconduit.name = Pulsleitungsrohr +block.pulseconduit.fulldescription = Fortschrittlicher Flüssigkeitstransportblock. Transportiert Flüssigkeiten schneller und speichert mehr als normale Leitungsrohre. +block.liquidrouter.name = flüssigkeiten verteiler +block.liquidrouter.fulldescription = Funktioniert ähnlich wie ein Router. Akzeptiert Flüssigkeit von einer Seite und gibt sie auf die anderen Seiten aus. Nützlich zum Aufspalten von Flüssigkeit aus eines einzelnen Leitungsrohres in mehrere andere Leitungensrohre. +block.conveyor.name = Förderband +block.conveyor.fulldescription = Grundelement Transport Block. Bewegt Gegenstände nach vorne und legt sie automatisch in Türmen oder ähnliches. Drehbar. Kann als Brücke für Gegner und Spieler verwendet werden. +block.steelconveyor.name = Stahlförderband +block.steelconveyor.fulldescription = Erweiterter Transportblock Bewegt Gegenstände schneller als Standardförderer. +block.poweredconveyor.name = Impulsförderband +block.poweredconveyor.fulldescription = Der ultimative Transportblock für Gegenstände. Bewegt Gegenstände noch schneller als Stahlförderer. +block.router.name = Verteiler +block.router.fulldescription = Akzeptiert Objekte aus einer Richtung und gibt sie in 3 andere Richtungen aus. Kann auch eine bestimmte Anzahl von Gegenständen speichern. Geeignet zum Aufteilen der Materialien von einem Bohrer in mehrere Geschütze. +block.junction.name = Kreuzung +block.junction.fulldescription = Fungiert als Brücke für zwei kreuzende Förderbänder. Nützlich in Situationen mit zwei verschiedenen Förderbänder, die unterschiedliche Materialien zu verschiedenen Orten transportieren. +block.conveyortunnel.name = Förderbandtunnel +block.conveyortunnel.fulldescription = Transportiert Artikel unter Blöcken. Verwendung für einen Tunnel, der in den zu untertunnelnden Block führt, und einen auf der anderen Seite. Stellen Sie sicher, dass beide Tunnel in entgegengesetzte Richtungen weisen, das heisst das die Tunnel voneinander weggucken. +block.liquidjunction.name = flüssigkeite Kreuzung +block.liquidjunction.fulldescription = Funktioniert als Brücke für zwei kreuzende Leitungsrohre. Nützlich in Situationen mit zwei verschiedenen Leitungen, die unterschiedliche Flüssigkeiten zu verschiedenen Orten transportieren. +block.liquiditemjunction.name = Flüssigkeit-Gegenstand-Kreuzung +block.liquiditemjunction.fulldescription = Fungiert als Brücke zum Überqueren von Leitungsrohre und Förderbändern. +block.powerbooster.name = Energieverstärker +block.powerbooster.fulldescription = Verteilt die Energie an alle Blöcke innerhalb seines Radius. +block.powerlaser.name = Energielaser +block.powerlaser.fulldescription = Erzeugt einen Laser, der die Kraft auf den Block davor überträgt. Erzeugt selbst keine Energie. Am besten mit Generatoren oder anderen Lasern verwendet. +block.powerlaserrouter.name = Laser Verteiler +block.powerlaserrouter.fulldescription = Laser, der die Kraft in drei Richtungen gleichzeitig verteilt. Nützlich in Situationen, in denen mehrere Blöcke von einem Generator mit Strom versorgt werden müssen. +block.powerlasercorner.name = Laser-Ecke +block.powerlasercorner.fulldescription = Laser, der die Kraft in zwei Richtungen gleichzeitig verteilt. Nützlich in Situationen, in denen mehrere Blöcke von einem Generator mit Strom versorgt werden müssen und ein Router ungenau ist. +block.teleporter.name = Teleporter +block.teleporter.fulldescription = Erweiterter Transportblock Teleporter geben Gegenstände in andere Teleporter derselben Farbe ein. Tut nichts, wenn keine Teleporter derselben Farbe existieren. Wenn mehrere Teleporter mit derselben Farbe existieren, wird eine zufällige ausgewählt. Verwendet Energie. Tippen, um die Farbe zu ändern. +block.sorter.name = Sortierer +block.sorter.fulldescription = Sortiert den Gegenstand nach Materialart. Das zu akzeptierende Material wird durch die Farbe im Block angezeigt. Alle Artikel, die dem Sortiermaterial entsprechen, werden vorwärts ausgegeben, alles andere wird nach links und rechts ausgegeben. +block.core.name = Kern +block.pump.name = Pumpe +block.pump.fulldescription = Pumpen Flüssigkeiten aus einem Quellblock - meist Wasser, Lava oder Öl. Gibt Flüssigkeit in benachbarte Leitungsrohre aus. +block.fluxpump.name = flux Pumpe +block.fluxpump.fulldescription = Eine erweiterte Version der Pumpe. Speichert mehr Flüssigkeit und pumpt Flüssigkeit schneller. +block.smelter.name = Schmelzer +block.smelter.fulldescription = Der essentielle Bastelblock. Wenn 1x Eisen und 1x Kohle eingegeben wird, wird 1x Stahl ausgegeben. +block.crucible.name = Tiegel +block.crucible.fulldescription = Ein fortgeschrittener Handwerksblock. Braucht Kohle um zu funktionieren. Wenn 1x Titan und 1x Stahl eingegeben wird, wird 1x Dirium ausgegeben. +block.coalpurifier.name = Kohle-Extraktor +block.coalpurifier.fulldescription = Ein einfacher Extraktorblock. Gibt Kohle aus, wenn der Block mit grossen Mengen Wasser und Stein gefüllt wird. +block.titaniumpurifier.name = Titan-Extraktor +block.titaniumpurifier.fulldescription = Ein Standard-Extraktorblock. Gibt Titan aus wenn er mit grossen Mengen Wasser und Eisen gefüllt wird. +block.oilrefinery.name = Ölraffinerie +block.oilrefinery.fulldescription = Veredelt grosse Mengen Öl zu Kohle. Nützlich für die Betankung von Blöcken die Kohle benutzen wie z.b. Waffentürme, wenn Kohleadern knapp sind. +block.stoneformer.name = Steinformer +block.stoneformer.fulldescription = Verfestigt flüssige Lava zu Stein. Nützlich für die Herstellung von grossen Mengen von Stein für Kohle-Extraktor. +block.lavasmelter.name = Lava-Schmelzer +block.lavasmelter.fulldescription = Verwendet Lava, um Eisen zu Stahl schmelzen. Eine Alternative zum Schmelzer. Nützlich in Situationen, in denen Kohle knapp ist. +block.stonedrill.name = Steinbohrer +block.stonedrill.fulldescription = Der wesentliche Bohrer. Wenn er auf Steinplatten gelegt wird, gibt er Steine ​​mit einer langsamen Geschwindigkeit auf endlosen Zeit aus. +block.irondrill.name = Eisenbohrer +block.irondrill.fulldescription = Eine grundlegender Bohrer. Wenn es auf Eisenerz gelegt wird, gibt er Eisen auf endloser Zeit langsam aus. +block.coaldrill.name = Kohlenbohrer +block.coaldrill.fulldescription = Eine grundlegender Bohrer. Wenn es auf Kohleerz platziert wird, gibt er für endloser Zeit langsam Kohle aus. +block.uraniumdrill.name = Uran-Bohrer +block.uraniumdrill.fulldescription = Ein fortgeschrittener Bohrer. Wenn es auf Uranerz platziert wird, gibt er Uran mit einer langsamen Geschwindigkeit auf endloser Zeit ab. +block.titaniumdrill.name = Titan-Bohrer +block.titaniumdrill.fulldescription = Ein fortgeschrittener Bohrer. Wenn es auf Titanerz platziert wird, gibt er Titan langsam aus für undendlich langer Zeit +block.omnidrill.name = Omni-Bohrer +block.omnidrill.fulldescription = Der ultimative Bohrer. Baut in schnellem Tempo jegliches Erz ab. +block.coalgenerator.name = Kohle-Generator +block.coalgenerator.fulldescription = Der wesentliche Generator. Erzeugt Energie aus Kohle. Gibt Energie als Laser an seine 4 Seiten aus. +block.thermalgenerator.name = thermischer Generator +block.thermalgenerator.fulldescription = Erzeugt Energie aus Lava. Gibt Energie als Laser an seine 4 Seiten aus. +block.combustiongenerator.name = Verbrennungsgenerator +block.combustiongenerator.fulldescription = Erzeugt Energie aus Öl. Gibt Energie als Laser an seine 4 Seiten aus. +block.rtgenerator.name = RTG-Generator +block.rtgenerator.fulldescription = Erzeugt geringe Mengen an Energie aus dem radioaktiven Zerfall von Uran. Gibt Energie als Laser an seine 4 Seiten aus. +block.nuclearreactor.name = Kernreaktor +block.nuclearreactor.fulldescription = Eine erweiterte Version des RTG-Generators und der ultimative Energie Generator. Erzeugt Strom aus Uran. Erfordert konstante Wasserkühlung. Sehr heiss; explodiert heftig, wenn zu wenig Kühlmittel zugeführt wird. +block.turret.name = Geschütz +block.turret.fulldescription = Ein einfacher, billiger Turm. Verwendet Stein für Munition. Hat etwas mehr Reichweite als das Doppelgeschütz. +block.doubleturret.name = Doppelgeschütz +block.doubleturret.fulldescription = Eine etwas stärkere Version des Geschützes. Verwendet Stein für Munition. Hat deutlich mehr Schaden, hat aber eine geringere Reichweite. Schiesst zwei Kugeln. +block.machineturret.name = Gatling Geschütz +block.machineturret.fulldescription = Ein Standard-Allround-Turm. Verwendet Eisen für Munition. Hat eine schnelle Feuerrate mit gutem Schaden. +block.shotgunturret.name = Splittergeschütz +block.shotgunturret.fulldescription = Ein Standard-Turm. Verwendet Eisen für Munition. Schiesst 7 Kugeln auf einmal. Geringere Reichweite, aber höhere Schadensleistung als das Gatling Geschütz +block.flameturret.name = Flammenwerfer +block.flameturret.fulldescription = Fortschrittlicher Nahbereichswaffe. Verwendet Kohle für Munition. Hat eine sehr geringe Reichweite, aber sehr hohen Schaden. Gut für Nahkampf. Empfohlen für den Einsatz hinter Mauern. +block.sniperturret.name = Schienenkanone +block.sniperturret.fulldescription = Fortschrittliches Reichweitengeschütz. Verwendet Stahl für Munition. Sehr hoher Schaden, aber niedrige Feuerrate. Teuer zu verwenden, kann aber aufgrund seiner Reichweite weit entfernt von den feindlichen Linien platziert werden. +block.mortarturret.name = Flakgeschütz +block.mortarturret.fulldescription = Fortschrittlicher Flächen-Schaden Turm. Verwendet Kohle für Munition. Sehr langsame Feuerrate und Geschosse, aber sehr hoher Einzelziel- und Flächenschaden. Nützlich gegen grosse Mengen von Feinden. +block.laserturret.name = Laserturm +block.laserturret.fulldescription = Fortschrittlicher Einzelziel-Turm. Verwendet Strom. Guter Allround-Revolver für mittlere Reichweiten. Nur Einzelziel. Verfehlt nie. +block.waveturret.name = Teslakanone +block.waveturret.fulldescription = Erweiterter Mehrfach-Ziele-Turm. Verwendet Strom. Mittlere Reichweite. Verfehlt nie. Im Durchschnitt zu wenig Schaden, aber kann mehrere Feinde gleichzeitig mit Kettenblitz treffen. +block.plasmaturret.name = Plasmageschütz +block.plasmaturret.fulldescription = Hochentwickelte Version des Flammenwerfers. Verwendet Kohle als Munition. Sehr hoher Schaden, niedriger bis mittlerer Reichweite. +block.chainturret.name = Kettengeschütz +block.chainturret.fulldescription = Die ultimative Schnellfeuer Waffe. Verwendet Uran als Munition. Schiesst grosse Kugeln mit hoher Feuerrate. Mittlere Reichweite. Mehrere Felder gross. Hält extrem viel aus. +block.titancannon.name = Titan Kanone +block.titancannon.fulldescription = Die ultimative Langstrecken Kanone. Verwendet Uran als Munition. Schiesst grosse Flächen-Schadenden-Granaten mit einer mittleren Feuerrate ab. Lange Reichweite. Ist mehrere Felder gross. Hält extrem viel aus. +block.playerspawn.name = Spielerspawn +block.enemyspawn.name = Gegnerspawn diff --git a/core/assets/sprites/sprites.atlas b/core/assets/sprites/sprites.atlas index 5b47048b1f..3fecd1d35d 100644 --- a/core/assets/sprites/sprites.atlas +++ b/core/assets/sprites/sprites.atlas @@ -13,1715 +13,1631 @@ backgrounds/background index: -1 blank rotate: false - xy: 231, 105 + xy: 862, 491 size: 1, 1 orig: 1, 1 offset: 0, 0 index: -1 blocks/blackrock1 rotate: false - xy: 221, 52 + xy: 132, 88 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/blackrockshadow1 rotate: false - xy: 399, 61 + xy: 844, 400 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/blackstone1 rotate: false - xy: 219, 42 + xy: 856, 405 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/blackstone2 rotate: false - xy: 171, 4 + xy: 659, 282 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/blackstone3 rotate: false - xy: 231, 47 + xy: 669, 282 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/blackstoneblock1 rotate: false - xy: 241, 47 + xy: 679, 282 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/blackstoneblock2 rotate: false - xy: 251, 47 + xy: 624, 233 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/blackstoneblock3 rotate: false - xy: 261, 47 + xy: 624, 223 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/blackstoneedge rotate: false - xy: 191, 44 + xy: 519, 178 size: 12, 12 orig: 12, 12 offset: 0, 0 index: -1 blocks/block rotate: false - xy: 291, 47 + xy: 634, 225 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/block-2x2 rotate: false - xy: 763, 426 + xy: 114, 90 size: 16, 16 orig: 16, 16 offset: 0, 0 index: -1 blocks/block-3x3 rotate: false - xy: 234, 105 + xy: 300, 105 size: 24, 24 orig: 24, 24 offset: 0, 0 index: -1 blocks/block-middle rotate: false - xy: 301, 47 + xy: 634, 215 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/chainturret rotate: false - xy: 727, 365 + xy: 659, 292 size: 16, 16 orig: 16, 16 offset: 0, 0 index: -1 blocks/chainturret-icon rotate: false - xy: 1013, 471 + xy: 580, 233 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 blocks/coal1 rotate: false - xy: 341, 49 + xy: 270, 91 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/coal2 rotate: false - xy: 351, 49 + xy: 280, 91 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/coal3 rotate: false - xy: 361, 49 + xy: 290, 91 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/coaldrill rotate: false - xy: 371, 49 + xy: 300, 95 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/coalgenerator rotate: false - xy: 381, 49 + xy: 310, 95 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/coalgenerator-top rotate: false - xy: 391, 49 + xy: 320, 95 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/coalpurifier rotate: false - xy: 401, 51 + xy: 330, 95 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/combustiongenerator rotate: false - xy: 584, 214 + xy: 340, 95 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/compositewall rotate: false - xy: 584, 204 + xy: 350, 95 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/conduit rotate: false - xy: 584, 194 + xy: 360, 95 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/conduitbottom rotate: false - xy: 584, 184 + xy: 370, 95 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/conduitliquid rotate: false - xy: 594, 215 + xy: 380, 95 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/conduittop rotate: false - xy: 594, 205 + xy: 390, 95 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/conveyor rotate: false - xy: 594, 195 + xy: 400, 95 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/conveyormove rotate: false - xy: 594, 185 + xy: 410, 95 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/conveyortunnel rotate: false - xy: 594, 175 + xy: 420, 95 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/core rotate: false - xy: 286, 105 + xy: 352, 105 size: 24, 24 orig: 24, 24 offset: 0, 0 index: -1 blocks/cross rotate: false - xy: 583, 165 + xy: 430, 95 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/crucible rotate: false - xy: 593, 165 + xy: 440, 95 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/deepwater rotate: false - xy: 604, 211 + xy: 450, 95 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/dirt1 rotate: false - xy: 604, 201 + xy: 460, 95 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/dirt2 rotate: false - xy: 604, 191 + xy: 470, 95 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/dirt3 rotate: false - xy: 604, 181 + xy: 113, 71 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/dirtedge rotate: false - xy: 207, 58 + xy: 535, 195 size: 12, 12 orig: 12, 12 offset: 0, 0 index: -1 blocks/door rotate: false - xy: 614, 211 + xy: 113, 61 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/door-large rotate: false - xy: 213, 90 + xy: 747, 430 size: 16, 16 orig: 16, 16 offset: 0, 0 index: -1 blocks/door-large-icon rotate: false - xy: 614, 201 + xy: 113, 51 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/door-large-open rotate: false - xy: 416, 113 + xy: 881, 495 size: 16, 16 orig: 16, 16 offset: 0, 0 index: -1 blocks/door-open rotate: false - xy: 614, 191 + xy: 132, 78 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/doubleturret rotate: false - xy: 1013, 447 + xy: 580, 221 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 blocks/duriumwall rotate: false - xy: 614, 181 + xy: 142, 81 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/duriumwall-large rotate: false - xy: 655, 292 + xy: 677, 292 size: 16, 16 orig: 16, 16 offset: 0, 0 index: -1 blocks/duriumwall-large-icon rotate: false - xy: 604, 171 + xy: 152, 81 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/enemyspawn rotate: false - xy: 614, 171 + xy: 162, 81 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/flameturret rotate: false - xy: 445, 71 + xy: 81, 2 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 blocks/fluxpump rotate: false - xy: 613, 161 + xy: 117, 41 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/grass1 rotate: false - xy: 624, 215 + xy: 117, 31 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/grass2 rotate: false - xy: 624, 205 + xy: 117, 21 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/grass3 rotate: false - xy: 624, 195 + xy: 117, 11 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/grassblock1 rotate: false - xy: 624, 185 + xy: 117, 1 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/grassblock2 rotate: false - xy: 624, 175 + xy: 561, 203 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/grassedge rotate: false - xy: 205, 44 + xy: 551, 213 size: 12, 12 orig: 12, 12 offset: 0, 0 index: -1 blocks/ice1 rotate: false - xy: 547, 79 + xy: 549, 194 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/ice2 rotate: false - xy: 183, 28 + xy: 559, 193 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/ice3 rotate: false - xy: 183, 18 + xy: 547, 184 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/iceedge rotate: false - xy: 421, 83 + xy: 565, 213 size: 12, 12 orig: 12, 12 offset: 0, 0 index: -1 blocks/icerock1 rotate: false - xy: 793, 426 + xy: 557, 183 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/icerock2 rotate: false - xy: 803, 426 + xy: 724, 377 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/icerockshadow1 rotate: false - xy: 817, 430 + xy: 724, 367 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/rockshadow1 rotate: false - xy: 817, 430 + xy: 724, 367 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/icerockshadow2 rotate: false - xy: 827, 430 + xy: 724, 357 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/rockshadow2 rotate: false - xy: 827, 430 + xy: 724, 357 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/iron1 rotate: false - xy: 805, 396 + xy: 535, 120 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/iron2 rotate: false - xy: 805, 386 + xy: 832, 388 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/iron3 rotate: false - xy: 785, 376 + xy: 300, 85 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/irondrill rotate: false - xy: 795, 376 + xy: 310, 85 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/ironwall rotate: false - xy: 805, 376 + xy: 320, 85 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/junction rotate: false - xy: 624, 165 + xy: 330, 85 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/laserturret rotate: false - xy: 805, 436 + xy: 844, 410 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 blocks/lava rotate: false - xy: 557, 79 + xy: 340, 85 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/lavaedge rotate: false - xy: 435, 83 + xy: 533, 178 size: 12, 12 orig: 12, 12 offset: 0, 0 index: -1 blocks/lavasmelter rotate: false - xy: 529, 73 + xy: 350, 85 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/liquiditemjunction rotate: false - xy: 193, 34 + xy: 360, 85 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/liquidjunction rotate: false - xy: 193, 24 + xy: 370, 85 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/liquidrouter rotate: false - xy: 203, 34 + xy: 380, 85 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/machineturret rotate: false - xy: 817, 440 + xy: 856, 415 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 blocks/megarepairturret rotate: false - xy: 829, 440 + xy: 530, 106 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 blocks/mortarturret rotate: false - xy: 841, 441 + xy: 736, 392 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 blocks/mossblock rotate: false - xy: 203, 24 + xy: 390, 85 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/mossstone rotate: false - xy: 203, 24 + xy: 390, 85 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/nuclearreactor rotate: false - xy: 312, 105 + xy: 378, 105 size: 24, 24 orig: 24, 24 offset: 0, 0 index: -1 blocks/nuclearreactor-center rotate: false - xy: 338, 105 + xy: 404, 105 size: 24, 24 orig: 24, 24 offset: 0, 0 index: -1 blocks/nuclearreactor-icon rotate: false - xy: 193, 14 + xy: 400, 85 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/nuclearreactor-lights rotate: false - xy: 364, 105 + xy: 430, 105 size: 24, 24 orig: 24, 24 offset: 0, 0 index: -1 blocks/nuclearreactor-small rotate: false - xy: 375, 87 + xy: 482, 113 size: 16, 16 orig: 16, 16 offset: 0, 0 index: -1 blocks/oil rotate: false - xy: 203, 14 + xy: 410, 85 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/oiledge rotate: false - xy: 477, 83 + xy: 521, 132 size: 12, 12 orig: 12, 12 offset: 0, 0 index: -1 blocks/oilrefinery rotate: false - xy: 183, 8 + xy: 420, 85 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/omnidrill rotate: false - xy: 193, 4 + xy: 430, 85 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/plasmaturret rotate: false - xy: 853, 441 + xy: 748, 394 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 blocks/playerspawn rotate: false - xy: 203, 4 + xy: 440, 85 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/powerbooster rotate: false - xy: 837, 430 + xy: 450, 85 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/poweredconveyor rotate: false - xy: 847, 431 + xy: 460, 85 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/poweredconveyormove rotate: false - xy: 857, 431 - size: 8, 8 - orig: 8, 8 - offset: 0, 0 - index: -1 -blocks/powerinfinite - rotate: false - xy: 815, 420 + xy: 470, 85 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/powerlaser rotate: false - xy: 815, 410 + xy: 480, 87 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/powerlasercorner rotate: false - xy: 825, 420 + xy: 172, 85 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/powerlaserrouter rotate: false - xy: 815, 400 - size: 8, 8 - orig: 8, 8 - offset: 0, 0 - index: -1 -blocks/powervoid - rotate: false - xy: 825, 410 + xy: 182, 87 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/pulseconduit rotate: false - xy: 835, 420 + xy: 192, 87 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/pulseconduitbottom rotate: false - xy: 815, 390 + xy: 202, 87 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/pulseconduittop rotate: false - xy: 825, 400 - size: 8, 8 - orig: 8, 8 - offset: 0, 0 - index: -1 -blocks/pulverizer - rotate: false - xy: 835, 410 + xy: 212, 87 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/pump rotate: false - xy: 815, 380 - size: 8, 8 - orig: 8, 8 - offset: 0, 0 - index: -1 -blocks/quartzextractor - rotate: false - xy: 825, 390 + xy: 222, 87 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/repairturret rotate: false - xy: 315, 57 + xy: 760, 394 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 blocks/rock1 rotate: false - xy: 835, 400 + xy: 728, 332 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/rock2 rotate: false - xy: 825, 380 + xy: 728, 322 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/router rotate: false - xy: 835, 390 + xy: 728, 312 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/rtgenerator rotate: false - xy: 835, 380 + xy: 738, 332 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/rtgenerator-top rotate: false - xy: 213, 32 + xy: 738, 322 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/sand1 rotate: false - xy: 213, 22 + xy: 738, 312 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/sand2 rotate: false - xy: 213, 12 + xy: 731, 302 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/sand3 rotate: false - xy: 213, 2 + xy: 731, 292 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/sandblock1 rotate: false - xy: 223, 32 + xy: 741, 302 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/sandblock2 rotate: false - xy: 223, 22 + xy: 741, 292 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/sandblock3 rotate: false - xy: 223, 12 + xy: 547, 140 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/sandedge rotate: false - xy: 491, 83 + xy: 142, 91 size: 12, 12 orig: 12, 12 offset: 0, 0 index: -1 blocks/shadow rotate: false - xy: 327, 59 + xy: 772, 394 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 blocks/shieldgenerator rotate: false - xy: 233, 37 + xy: 545, 120 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/shotgunturret rotate: false - xy: 339, 59 + xy: 784, 394 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 blocks/shrub rotate: false - xy: 253, 37 + xy: 202, 77 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/shrubshadow rotate: false - xy: 233, 7 - size: 8, 8 - orig: 8, 8 - offset: 0, 0 - index: -1 -blocks/siliconextractor - rotate: false - xy: 243, 17 + xy: 212, 77 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/smelter rotate: false - xy: 253, 27 + xy: 222, 77 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/smelter-middle rotate: false - xy: 263, 37 + xy: 744, 382 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/sniperturret rotate: false - xy: 351, 59 + xy: 796, 394 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 blocks/snow1 rotate: false - xy: 243, 7 + xy: 744, 372 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/snow2 rotate: false - xy: 253, 17 + xy: 744, 362 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/snow3 rotate: false - xy: 263, 27 + xy: 744, 352 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/snowblock1 rotate: false - xy: 273, 37 + xy: 744, 342 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/snowblock2 rotate: false - xy: 253, 7 + xy: 754, 384 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/snowblock3 rotate: false - xy: 263, 17 + xy: 764, 384 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/snowedge rotate: false - xy: 519, 83 + xy: 521, 118 size: 12, 12 orig: 12, 12 offset: 0, 0 index: -1 blocks/sorter rotate: false - xy: 273, 27 - size: 8, 8 - orig: 8, 8 - offset: 0, 0 - index: -1 -blocks/splitter - rotate: false - xy: 283, 37 + xy: 754, 374 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/steelconveyor rotate: false - xy: 263, 7 + xy: 774, 384 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/steelconveyormove rotate: false - xy: 273, 17 + xy: 754, 364 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/steelwall rotate: false - xy: 283, 27 + xy: 764, 374 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/steelwall-large rotate: false - xy: 393, 87 + xy: 819, 430 size: 16, 16 orig: 16, 16 offset: 0, 0 index: -1 blocks/steelwall-large-icon rotate: false - xy: 293, 37 + xy: 784, 384 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/stone1 rotate: false - xy: 273, 7 + xy: 754, 354 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/stone2 rotate: false - xy: 283, 17 + xy: 764, 364 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/stone3 rotate: false - xy: 293, 27 + xy: 774, 374 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/stoneblock1 rotate: false - xy: 303, 37 + xy: 794, 384 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/stoneblock2 rotate: false - xy: 283, 7 + xy: 754, 344 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/stoneblock3 rotate: false - xy: 293, 17 + xy: 764, 354 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/stonedrill rotate: false - xy: 303, 27 + xy: 774, 364 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/stoneedge rotate: false - xy: 417, 69 + xy: 722, 413 size: 12, 12 orig: 12, 12 offset: 0, 0 index: -1 blocks/stoneformer rotate: false - xy: 313, 37 + xy: 784, 374 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/stonewall rotate: false - xy: 293, 7 + xy: 804, 384 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/teleporter rotate: false - xy: 303, 17 + xy: 764, 344 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/teleporter-top rotate: false - xy: 313, 27 + xy: 774, 354 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/thermalgenerator rotate: false - xy: 303, 7 + xy: 784, 364 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/titancannon rotate: false - xy: 390, 105 + xy: 456, 105 size: 24, 24 orig: 24, 24 offset: 0, 0 index: -1 blocks/titancannon-icon rotate: false - xy: 363, 59 + xy: 808, 394 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 blocks/titanium1 rotate: false - xy: 313, 17 + xy: 794, 374 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/titanium2 rotate: false - xy: 313, 7 + xy: 814, 384 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/titanium3 rotate: false - xy: 323, 37 + xy: 774, 344 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/titaniumdrill rotate: false - xy: 323, 27 + xy: 784, 354 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/titaniumpurifier rotate: false - xy: 323, 17 + xy: 794, 364 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/titaniumshieldwall rotate: false - xy: 323, 7 + xy: 804, 374 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/titaniumwall rotate: false - xy: 333, 39 + xy: 784, 344 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/titaniumwall-large rotate: false - xy: 285, 69 + xy: 831, 472 size: 16, 16 orig: 16, 16 offset: 0, 0 index: -1 blocks/titaniumwall-large-icon rotate: false - xy: 333, 29 + xy: 794, 354 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/turret rotate: false - xy: 375, 59 + xy: 820, 394 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 blocks/uranium1 rotate: false - xy: 353, 39 + xy: 804, 354 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/uranium2 rotate: false - xy: 333, 9 + xy: 814, 364 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/uranium3 rotate: false - xy: 343, 19 + xy: 804, 344 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/uraniumdrill rotate: false - xy: 353, 29 + xy: 814, 354 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/water rotate: false - xy: 353, 19 + xy: 748, 322 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 blocks/wateredge rotate: false - xy: 431, 69 + xy: 722, 399 size: 12, 12 orig: 12, 12 offset: 0, 0 index: -1 blocks/waveturret rotate: false - xy: 387, 59 + xy: 832, 398 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 blocks/weaponfactory rotate: false - xy: 303, 69 + xy: 831, 454 size: 16, 16 orig: 16, 16 offset: 0, 0 index: -1 blocks/weaponfactory-icon rotate: false - xy: 363, 29 + xy: 748, 312 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 bullet rotate: false - xy: 311, 47 + xy: 240, 91 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 chainbullet rotate: false - xy: 584, 175 + xy: 114, 81 size: 8, 7 orig: 8, 7 offset: 0, 0 index: -1 -enemies/blastenemy-t1 +circle rotate: false - xy: 321, 71 - size: 14, 14 - orig: 14, 14 + xy: 862, 494 + size: 17, 17 + orig: 17, 17 offset: 0, 0 index: -1 -enemies/blastenemy-t2 - rotate: false - xy: 337, 71 - size: 14, 14 - orig: 14, 14 - offset: 0, 0 - index: -1 -enemies/blastenemy-t3 - rotate: false - xy: 353, 71 - size: 14, 14 - orig: 14, 14 - offset: 0, 0 - index: -1 -enemies/empenemy-t1 - rotate: false - xy: 369, 71 - size: 14, 14 - orig: 14, 14 - offset: 0, 0 - index: -1 -enemies/empenemy-t2 - rotate: false - xy: 385, 71 - size: 14, 14 - orig: 14, 14 - offset: 0, 0 - index: -1 -enemies/empenemy-t3 - rotate: false - xy: 524, 115 - size: 14, 14 - orig: 14, 14 - offset: 0, 0 - index: -1 -enemies/fastenemy-t1 - rotate: false - xy: 540, 119 - size: 14, 14 - orig: 14, 14 - offset: 0, 0 - index: -1 -enemies/fastenemy-t2 - rotate: false - xy: 641, 276 - size: 14, 14 - orig: 14, 14 - offset: 0, 0 - index: -1 -enemies/fastenemy-t3 - rotate: false - xy: 641, 260 - size: 14, 14 - orig: 14, 14 - offset: 0, 0 - index: -1 -enemies/flamerenemy-t1 - rotate: false - xy: 657, 276 - size: 14, 14 - orig: 14, 14 - offset: 0, 0 - index: -1 -enemies/flamerenemy-t2 - rotate: false - xy: 657, 260 - size: 14, 14 - orig: 14, 14 - offset: 0, 0 - index: -1 -enemies/flamerenemy-t3 - rotate: false - xy: 673, 276 - size: 14, 14 - orig: 14, 14 - offset: 0, 0 - index: -1 -enemies/fortressenemy-t1 - rotate: false - xy: 131, 44 - size: 20, 20 - orig: 20, 20 - offset: 0, 0 - index: -1 -enemies/fortressenemy-t2 - rotate: false - xy: 153, 44 - size: 20, 20 - orig: 20, 20 - offset: 0, 0 - index: -1 -enemies/fortressenemy-t3 - rotate: false - xy: 537, 135 - size: 20, 20 - orig: 20, 20 - offset: 0, 0 - index: -1 -enemies/healerenemy-t1 - rotate: false - xy: 673, 260 - size: 14, 14 - orig: 14, 14 - offset: 0, 0 - index: -1 -enemies/healerenemy-t2 - rotate: false - xy: 641, 244 - size: 14, 14 - orig: 14, 14 - offset: 0, 0 - index: -1 -enemies/healerenemy-t3 - rotate: false - xy: 657, 244 - size: 14, 14 - orig: 14, 14 - offset: 0, 0 - index: -1 -enemies/mortarenemy-t1 - rotate: false - xy: 416, 97 - size: 14, 14 - orig: 14, 14 - offset: 0, 0 - index: -1 -enemies/mortarenemy-t2 - rotate: false - xy: 432, 97 - size: 14, 14 - orig: 14, 14 - offset: 0, 0 - index: -1 -enemies/mortarenemy-t3 - rotate: false - xy: 448, 97 - size: 14, 14 - orig: 14, 14 - offset: 0, 0 - index: -1 -enemies/rapidenemy-t1 - rotate: false - xy: 464, 97 - size: 14, 14 - orig: 14, 14 - offset: 0, 0 - index: -1 -enemies/rapidenemy-t2 - rotate: false - xy: 480, 97 - size: 14, 14 - orig: 14, 14 - offset: 0, 0 - index: -1 -enemies/rapidenemy-t3 - rotate: false - xy: 496, 97 - size: 14, 14 - orig: 14, 14 - offset: 0, 0 - index: -1 -enemies/standardenemy-t1 - rotate: false - xy: 512, 97 - size: 14, 14 - orig: 14, 14 - offset: 0, 0 - index: -1 -enemies/targetenemy-t1 - rotate: false - xy: 512, 97 - size: 14, 14 - orig: 14, 14 - offset: 0, 0 - index: -1 -enemies/standardenemy-t2 - rotate: false - xy: 533, 85 - size: 12, 12 - orig: 12, 12 - offset: 0, 0 - index: -1 -enemies/standardenemy-t3 - rotate: false - xy: 547, 89 - size: 12, 12 - orig: 12, 12 - offset: 0, 0 - index: -1 -enemies/tankenemy-t1 - rotate: false - xy: 528, 99 - size: 14, 14 - orig: 14, 14 - offset: 0, 0 - index: -1 -enemies/tankenemy-t2 - rotate: false - xy: 191, 58 - size: 14, 14 - orig: 14, 14 - offset: 0, 0 - index: -1 -enemies/tankenemy-t3 - rotate: false - xy: 544, 103 - size: 14, 14 - orig: 14, 14 - offset: 0, 0 - index: -1 -enemies/titanenemy-t1 - rotate: false - xy: 231, 69 - size: 16, 16 - orig: 16, 16 - offset: 0, 0 - index: -1 -enemies/titanenemy-t2 - rotate: false - xy: 249, 69 - size: 16, 16 - orig: 16, 16 - offset: 0, 0 - index: -1 -enemies/titanenemy-t3 - rotate: false - xy: 267, 69 - size: 16, 16 - orig: 16, 16 - offset: 0, 0 - index: -1 -enemyarrow - rotate: false - xy: 603, 162 - size: 8, 7 - orig: 8, 7 - offset: 0, 0 - index: -1 -icon-coal - rotate: false - xy: 785, 416 - size: 8, 8 - orig: 8, 8 - offset: 0, 0 - index: -1 -icon-dirium - rotate: false - xy: 785, 406 - size: 8, 8 - orig: 8, 8 - offset: 0, 0 - index: -1 -icon-glass - rotate: false - xy: 795, 416 - size: 8, 8 - orig: 8, 8 - offset: 0, 0 - index: -1 -icon-iron - rotate: false - xy: 785, 396 - size: 8, 8 - orig: 8, 8 - offset: 0, 0 - index: -1 -icon-quartz - rotate: false - xy: 795, 406 - size: 8, 8 - orig: 8, 8 - offset: 0, 0 - index: -1 -icon-silicon - rotate: false - xy: 785, 386 - size: 8, 8 - orig: 8, 8 - offset: 0, 0 - index: -1 -icon-steel - rotate: false - xy: 795, 396 - size: 8, 8 - orig: 8, 8 - offset: 0, 0 - index: -1 -icon-stone - rotate: false - xy: 795, 386 - size: 8, 8 - orig: 8, 8 - offset: 0, 0 - index: -1 -icon-titanium - rotate: false - xy: 805, 416 - size: 8, 8 - orig: 8, 8 - offset: 0, 0 - index: -1 -icon-uranium - rotate: false - xy: 805, 406 - size: 8, 8 - orig: 8, 8 - offset: 0, 0 - index: -1 -laser - rotate: false - xy: 580, 164 - size: 1, 12 - orig: 1, 12 - offset: 0, 0 - index: -1 -laserend - rotate: false - xy: 560, 158 - size: 18, 18 - orig: 18, 18 - offset: 0, 0 - index: -1 -laserfull - rotate: false - xy: 589, 225 - size: 18, 18 - orig: 18, 18 - offset: 0, 0 - index: -1 -mechs/mech-standard - rotate: false - xy: 449, 83 - size: 12, 12 - orig: 12, 12 - offset: 0, 0 - index: -1 -mechs/mech-standard-icon - rotate: false - xy: 463, 83 - size: 12, 12 - orig: 12, 12 - offset: 0, 0 - index: -1 -mechs/ship-standard - rotate: false - xy: 505, 83 - size: 12, 12 - orig: 12, 12 - offset: 0, 0 - index: -1 -shapes/circle +circle2 rotate: false xy: 495, 310 size: 201, 201 orig: 201, 201 offset: 0, 0 index: -1 -shapes/shape-3 +enemies/blastenemy-t1 rotate: false - xy: 1, 43 - size: 63, 63 - orig: 63, 63 + xy: 849, 475 + size: 14, 14 + orig: 14, 14 offset: 0, 0 index: -1 -shapes/shape-4 +enemies/blastenemy-t2 rotate: false - xy: 495, 222 - size: 63, 63 - orig: 63, 63 + xy: 849, 459 + size: 14, 14 + orig: 14, 14 offset: 0, 0 index: -1 -shapes/shape-5 +enemies/blastenemy-t3 rotate: false - xy: 698, 425 - size: 63, 63 - orig: 63, 63 + xy: 865, 478 + size: 14, 14 + orig: 14, 14 offset: 0, 0 index: -1 -shapes/shape-6 +enemies/empenemy-t1 rotate: false - xy: 66, 43 - size: 63, 63 - orig: 63, 63 + xy: 881, 479 + size: 14, 14 + orig: 14, 14 offset: 0, 0 index: -1 -shapes/shape-7 +enemies/empenemy-t2 rotate: false - xy: 495, 157 - size: 63, 63 - orig: 63, 63 + xy: 865, 462 + size: 14, 14 + orig: 14, 14 + offset: 0, 0 + index: -1 +enemies/empenemy-t3 + rotate: false + xy: 881, 463 + size: 14, 14 + orig: 14, 14 + offset: 0, 0 + index: -1 +enemies/fastenemy-t1 + rotate: false + xy: 897, 479 + size: 14, 14 + orig: 14, 14 + offset: 0, 0 + index: -1 +enemies/fastenemy-t2 + rotate: false + xy: 897, 463 + size: 14, 14 + orig: 14, 14 + offset: 0, 0 + index: -1 +enemies/fastenemy-t3 + rotate: false + xy: 913, 479 + size: 14, 14 + orig: 14, 14 + offset: 0, 0 + index: -1 +enemies/flamerenemy-t1 + rotate: false + xy: 913, 463 + size: 14, 14 + orig: 14, 14 + offset: 0, 0 + index: -1 +enemies/flamerenemy-t2 + rotate: false + xy: 929, 479 + size: 14, 14 + orig: 14, 14 + offset: 0, 0 + index: -1 +enemies/flamerenemy-t3 + rotate: false + xy: 929, 463 + size: 14, 14 + orig: 14, 14 + offset: 0, 0 + index: -1 +enemies/fortressenemy-t1 + rotate: false + xy: 840, 491 + size: 20, 20 + orig: 20, 20 + offset: 0, 0 + index: -1 +enemies/fortressenemy-t2 + rotate: false + xy: 92, 86 + size: 20, 20 + orig: 20, 20 + offset: 0, 0 + index: -1 +enemies/fortressenemy-t3 + rotate: false + xy: 637, 288 + size: 20, 20 + orig: 20, 20 + offset: 0, 0 + index: -1 +enemies/healerenemy-t1 + rotate: false + xy: 945, 479 + size: 14, 14 + orig: 14, 14 + offset: 0, 0 + index: -1 +enemies/healerenemy-t2 + rotate: false + xy: 945, 463 + size: 14, 14 + orig: 14, 14 + offset: 0, 0 + index: -1 +enemies/healerenemy-t3 + rotate: false + xy: 961, 479 + size: 14, 14 + orig: 14, 14 + offset: 0, 0 + index: -1 +enemies/mortarenemy-t1 + rotate: false + xy: 981, 431 + size: 14, 14 + orig: 14, 14 + offset: 0, 0 + index: -1 +enemies/mortarenemy-t2 + rotate: false + xy: 997, 447 + size: 14, 14 + orig: 14, 14 + offset: 0, 0 + index: -1 +enemies/mortarenemy-t3 + rotate: false + xy: 997, 431 + size: 14, 14 + orig: 14, 14 + offset: 0, 0 + index: -1 +enemies/rapidenemy-t1 + rotate: false + xy: 592, 211 + size: 14, 14 + orig: 14, 14 + offset: 0, 0 + index: -1 +enemies/rapidenemy-t2 + rotate: false + xy: 608, 211 + size: 14, 14 + orig: 14, 14 + offset: 0, 0 + index: -1 +enemies/rapidenemy-t3 + rotate: false + xy: 519, 208 + size: 14, 14 + orig: 14, 14 + offset: 0, 0 + index: -1 +enemies/standardenemy-t1 + rotate: false + xy: 519, 192 + size: 14, 14 + orig: 14, 14 + offset: 0, 0 + index: -1 +enemies/targetenemy-t1 + rotate: false + xy: 519, 192 + size: 14, 14 + orig: 14, 14 + offset: 0, 0 + index: -1 +enemies/standardenemy-t2 + rotate: false + xy: 516, 104 + size: 12, 12 + orig: 12, 12 + offset: 0, 0 + index: -1 +enemies/standardenemy-t3 + rotate: false + xy: 514, 90 + size: 12, 12 + orig: 12, 12 + offset: 0, 0 + index: -1 +enemies/tankenemy-t1 + rotate: false + xy: 521, 162 + size: 14, 14 + orig: 14, 14 + offset: 0, 0 + index: -1 +enemies/tankenemy-t2 + rotate: false + xy: 521, 146 + size: 14, 14 + orig: 14, 14 + offset: 0, 0 + index: -1 +enemies/tankenemy-t3 + rotate: false + xy: 535, 209 + size: 14, 14 + orig: 14, 14 + offset: 0, 0 + index: -1 +enemies/titanenemy-t1 + rotate: false + xy: 99, 32 + size: 16, 16 + orig: 16, 16 + offset: 0, 0 + index: -1 +enemies/titanenemy-t2 + rotate: false + xy: 99, 14 + size: 16, 16 + orig: 16, 16 + offset: 0, 0 + index: -1 +enemies/titanenemy-t3 + rotate: false + xy: 500, 122 + size: 16, 16 + orig: 16, 16 + offset: 0, 0 + index: -1 +enemyarrow + rotate: false + xy: 551, 204 + size: 8, 7 + orig: 8, 7 + offset: 0, 0 + index: -1 +icon-coal + rotate: false + xy: 724, 347 + size: 8, 8 + orig: 8, 8 + offset: 0, 0 + index: -1 +icon-dirium + rotate: false + xy: 734, 382 + size: 8, 8 + orig: 8, 8 + offset: 0, 0 + index: -1 +icon-iron + rotate: false + xy: 734, 372 + size: 8, 8 + orig: 8, 8 + offset: 0, 0 + index: -1 +icon-sand + rotate: false + xy: 734, 362 + size: 8, 8 + orig: 8, 8 + offset: 0, 0 + index: -1 +icon-steel + rotate: false + xy: 734, 352 + size: 8, 8 + orig: 8, 8 + offset: 0, 0 + index: -1 +icon-stone + rotate: false + xy: 734, 342 + size: 8, 8 + orig: 8, 8 + offset: 0, 0 + index: -1 +icon-titanium + rotate: false + xy: 537, 140 + size: 8, 8 + orig: 8, 8 + offset: 0, 0 + index: -1 +icon-uranium + rotate: false + xy: 535, 130 + size: 8, 8 + orig: 8, 8 + offset: 0, 0 + index: -1 +laser + rotate: false + xy: 518, 126 + size: 1, 12 + orig: 1, 12 + offset: 0, 0 + index: -1 +laserend + rotate: false + xy: 524, 225 + size: 18, 18 + orig: 18, 18 + offset: 0, 0 + index: -1 +laserfull + rotate: false + xy: 727, 428 + size: 18, 18 + orig: 18, 18 + offset: 0, 0 + index: -1 +mechs/mech-standard + rotate: false + xy: 537, 164 + size: 12, 12 + orig: 12, 12 + offset: 0, 0 + index: -1 +mechs/mech-standard-icon + rotate: false + xy: 537, 150 + size: 12, 12 + orig: 12, 12 + offset: 0, 0 + index: -1 +mechs/ship-standard + rotate: false + xy: 156, 91 + size: 12, 12 + orig: 12, 12 offset: 0, 0 index: -1 shell rotate: false - xy: 223, 2 + xy: 545, 130 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 shot rotate: false - xy: 233, 17 + xy: 182, 77 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 shot-long rotate: false - xy: 243, 27 + xy: 192, 77 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 titanshell rotate: false - xy: 343, 39 + xy: 804, 364 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 ui/border rotate: false - xy: 127, 1 + xy: 727, 448 size: 24, 40 split: 5, 5, 5, 10 orig: 24, 40 @@ -1729,7 +1645,7 @@ ui/border index: -1 ui/button rotate: false - xy: 131, 66 + xy: 753, 448 size: 24, 40 split: 10, 10, 6, 10 orig: 24, 40 @@ -1737,7 +1653,7 @@ ui/button index: -1 ui/button-down rotate: false - xy: 589, 245 + xy: 43, 43 size: 24, 40 split: 10, 10, 6, 10 orig: 24, 40 @@ -1745,7 +1661,7 @@ ui/button-down index: -1 ui/button-over rotate: false - xy: 589, 245 + xy: 43, 43 size: 24, 40 split: 10, 10, 6, 10 orig: 24, 40 @@ -1753,7 +1669,7 @@ ui/button-over index: -1 ui/button-map rotate: false - xy: 727, 383 + xy: 566, 245 size: 24, 40 split: 10, 10, 5, 10 orig: 24, 40 @@ -1761,7 +1677,7 @@ ui/button-map index: -1 ui/button-map-down rotate: false - xy: 698, 320 + xy: 43, 1 size: 24, 40 split: 10, 10, 5, 10 orig: 24, 40 @@ -1769,7 +1685,7 @@ ui/button-map-down index: -1 ui/button-map-over rotate: false - xy: 698, 320 + xy: 43, 1 size: 24, 40 split: 10, 10, 5, 10 orig: 24, 40 @@ -1777,7 +1693,7 @@ ui/button-map-over index: -1 ui/button-select rotate: false - xy: 260, 105 + xy: 326, 105 size: 24, 24 split: 4, 4, 4, 4 orig: 24, 24 @@ -1785,595 +1701,602 @@ ui/button-select index: -1 ui/check-off rotate: false - xy: 183, 74 + xy: 69, 14 size: 28, 32 orig: 28, 32 offset: 0, 0 index: -1 ui/check-on rotate: false - xy: 839, 453 + xy: 698, 309 size: 28, 32 orig: 28, 32 offset: 0, 0 index: -1 ui/check-on-over rotate: false - xy: 877, 479 + xy: 180, 97 size: 28, 32 orig: 28, 32 offset: 0, 0 index: -1 ui/check-over rotate: false - xy: 907, 479 + xy: 210, 97 size: 28, 32 orig: 28, 32 offset: 0, 0 index: -1 ui/clear rotate: false - xy: 1013, 459 + xy: 69, 2 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 ui/cursor rotate: false - xy: 781, 426 + xy: 831, 448 size: 4, 4 orig: 4, 4 offset: 0, 0 index: -1 ui/icons/controller-cursor rotate: false - xy: 637, 292 + xy: 544, 227 size: 16, 16 orig: 16, 16 offset: 0, 0 index: -1 ui/icons/icon-about rotate: false - xy: 673, 244 + xy: 961, 463 size: 14, 14 orig: 14, 14 offset: 0, 0 index: -1 ui/icons/icon-add rotate: false - xy: 698, 304 + xy: 977, 479 size: 14, 14 orig: 14, 14 offset: 0, 0 index: -1 +ui/icons/icon-admin + rotate: false + xy: 977, 463 + size: 14, 14 + orig: 14, 14 + offset: 0, 0 + index: -1 +ui/icons/icon-admin-small + rotate: false + xy: 124, 82 + size: 6, 6 + orig: 6, 6 + offset: 0, 0 + index: -1 ui/icons/icon-areaDelete rotate: false - xy: 457, 71 + xy: 93, 2 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 ui/icons/icon-arrow rotate: false - xy: 753, 409 + xy: 993, 479 size: 14, 14 orig: 14, 14 offset: 0, 0 index: -1 ui/icons/icon-arrow-down rotate: false - xy: 469, 71 + xy: 105, 2 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 ui/icons/icon-arrow-left rotate: false - xy: 481, 71 + xy: 1013, 451 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 ui/icons/icon-arrow-right rotate: false - xy: 493, 71 + xy: 1013, 439 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 ui/icons/icon-arrow-up rotate: false - xy: 505, 71 + xy: 1013, 427 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 ui/icons/icon-back rotate: false - xy: 434, 113 + xy: 562, 227 size: 16, 16 orig: 16, 16 offset: 0, 0 index: -1 +ui/icons/icon-ban + rotate: false + xy: 1009, 479 + size: 14, 14 + orig: 14, 14 + offset: 0, 0 + index: -1 ui/icons/icon-cancel rotate: false - xy: 753, 393 + xy: 993, 463 size: 14, 14 orig: 14, 14 offset: 0, 0 index: -1 ui/icons/icon-chat rotate: false - xy: 517, 71 + xy: 579, 209 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 ui/icons/icon-check rotate: false - xy: 769, 410 + xy: 1009, 463 size: 14, 14 orig: 14, 14 offset: 0, 0 index: -1 ui/icons/icon-close rotate: false - xy: 1, 1 + xy: 1, 43 size: 40, 40 orig: 40, 40 offset: 0, 0 index: -1 ui/icons/icon-close-down rotate: false - xy: 43, 1 + xy: 1, 1 size: 40, 40 orig: 40, 40 offset: 0, 0 index: -1 ui/icons/icon-close-over rotate: false - xy: 85, 1 + xy: 524, 245 size: 40, 40 orig: 40, 40 offset: 0, 0 index: -1 ui/icons/icon-crafting rotate: false - xy: 965, 439 + xy: 722, 387 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 ui/icons/icon-cursor rotate: false - xy: 977, 439 + xy: 736, 416 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 ui/icons/icon-defense rotate: false - xy: 989, 439 + xy: 736, 404 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 ui/icons/icon-discord rotate: false - xy: 769, 394 + xy: 592, 227 size: 14, 14 orig: 14, 14 offset: 0, 0 index: -1 ui/icons/icon-distribution rotate: false - xy: 1001, 439 + xy: 748, 418 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 ui/icons/icon-donate rotate: false - xy: 401, 71 + xy: 608, 227 size: 14, 14 orig: 14, 14 offset: 0, 0 index: -1 ui/icons/icon-dots rotate: false - xy: 714, 304 + xy: 500, 106 size: 14, 14 orig: 14, 14 offset: 0, 0 index: -1 ui/icons/icon-editor rotate: false - xy: 753, 377 + xy: 482, 97 size: 14, 14 orig: 14, 14 offset: 0, 0 index: -1 ui/icons/icon-file-text rotate: false - xy: 769, 378 + xy: 498, 90 size: 14, 14 orig: 14, 14 offset: 0, 0 index: -1 ui/icons/icon-fill rotate: false - xy: 673, 292 + xy: 765, 430 size: 16, 16 orig: 16, 16 offset: 0, 0 index: -1 ui/icons/icon-floppy rotate: false - xy: 869, 463 + xy: 628, 245 size: 14, 14 orig: 14, 14 offset: 0, 0 index: -1 ui/icons/icon-folder rotate: false - xy: 885, 463 + xy: 837, 438 size: 14, 14 orig: 14, 14 offset: 0, 0 index: -1 ui/icons/icon-folder-parent rotate: false - xy: 901, 463 + xy: 837, 422 size: 14, 14 orig: 14, 14 offset: 0, 0 index: -1 ui/icons/icon-grid rotate: false - xy: 452, 113 + xy: 899, 495 size: 16, 16 orig: 16, 16 offset: 0, 0 index: -1 ui/icons/icon-hold rotate: false - xy: 1013, 435 + xy: 760, 418 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 ui/icons/icon-holdDelete rotate: false - xy: 231, 57 + xy: 748, 406 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 ui/icons/icon-home rotate: false - xy: 917, 463 + xy: 853, 443 size: 14, 14 orig: 14, 14 offset: 0, 0 index: -1 ui/icons/icon-host rotate: false - xy: 869, 447 + xy: 853, 427 size: 14, 14 orig: 14, 14 offset: 0, 0 index: -1 ui/icons/icon-info rotate: false - xy: 243, 57 + xy: 772, 418 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 ui/icons/icon-line rotate: false - xy: 470, 113 + xy: 783, 430 size: 16, 16 orig: 16, 16 offset: 0, 0 index: -1 -ui/icons/icon-liquid - rotate: false - xy: 255, 57 - size: 10, 10 - orig: 10, 10 - offset: 0, 0 - index: -1 ui/icons/icon-load rotate: false - xy: 885, 447 + xy: 869, 446 size: 14, 14 orig: 14, 14 offset: 0, 0 index: -1 ui/icons/icon-load-image rotate: false - xy: 488, 113 + xy: 917, 495 size: 16, 16 orig: 16, 16 offset: 0, 0 index: -1 ui/icons/icon-load-map rotate: false - xy: 506, 113 + xy: 801, 430 size: 16, 16 orig: 16, 16 offset: 0, 0 index: -1 ui/icons/icon-loading rotate: false - xy: 153, 26 + xy: 935, 495 size: 16, 16 orig: 16, 16 offset: 0, 0 index: -1 -ui/icons/icon-logic - rotate: false - xy: 267, 57 - size: 10, 10 - orig: 10, 10 - offset: 0, 0 - index: -1 ui/icons/icon-menu rotate: false - xy: 279, 57 + xy: 760, 406 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 ui/icons/icon-none rotate: false - xy: 291, 57 + xy: 784, 418 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 ui/icons/icon-pause rotate: false - xy: 303, 57 + xy: 772, 406 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 ui/icons/icon-pencil rotate: false - xy: 153, 8 + xy: 953, 495 size: 16, 16 orig: 16, 16 offset: 0, 0 index: -1 ui/icons/icon-pencil-small rotate: false - xy: 901, 447 + xy: 869, 430 size: 14, 14 orig: 14, 14 offset: 0, 0 index: -1 ui/icons/icon-pick rotate: false - xy: 213, 72 + xy: 971, 495 size: 16, 16 orig: 16, 16 offset: 0, 0 index: -1 ui/icons/icon-play rotate: false - xy: 609, 233 + xy: 796, 418 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 ui/icons/icon-play-2 rotate: false - xy: 917, 447 + xy: 885, 447 size: 14, 14 orig: 14, 14 offset: 0, 0 index: -1 ui/icons/icon-players rotate: false - xy: 621, 237 + xy: 784, 406 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 ui/icons/icon-power rotate: false - xy: 609, 221 + xy: 808, 418 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 ui/icons/icon-production rotate: false - xy: 621, 225 + xy: 796, 406 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 ui/icons/icon-quit rotate: false - xy: 933, 460 + xy: 885, 431 size: 14, 14 orig: 14, 14 offset: 0, 0 index: -1 ui/icons/icon-redo rotate: false - xy: 231, 87 + xy: 989, 495 size: 16, 16 orig: 16, 16 offset: 0, 0 index: -1 ui/icons/icon-refresh rotate: false - xy: 949, 460 + xy: 901, 447 size: 14, 14 orig: 14, 14 offset: 0, 0 index: -1 ui/icons/icon-rename rotate: false - xy: 933, 444 + xy: 901, 431 size: 14, 14 orig: 14, 14 offset: 0, 0 index: -1 ui/icons/icon-resize rotate: false - xy: 249, 87 + xy: 1007, 495 size: 16, 16 orig: 16, 16 offset: 0, 0 index: -1 ui/icons/icon-rotate rotate: false - xy: 949, 444 + xy: 917, 447 size: 14, 14 orig: 14, 14 offset: 0, 0 index: -1 ui/icons/icon-rotate-arrow rotate: false - xy: 965, 467 + xy: 917, 431 size: 14, 14 orig: 14, 14 offset: 0, 0 index: -1 ui/icons/icon-rotate-left rotate: false - xy: 965, 451 + xy: 933, 447 size: 14, 14 orig: 14, 14 offset: 0, 0 index: -1 ui/icons/icon-rotate-right rotate: false - xy: 981, 467 + xy: 933, 431 size: 14, 14 orig: 14, 14 offset: 0, 0 index: -1 ui/icons/icon-save rotate: false - xy: 981, 451 + xy: 949, 447 size: 14, 14 orig: 14, 14 offset: 0, 0 index: -1 ui/icons/icon-save-image rotate: false - xy: 267, 87 + xy: 95, 68 size: 16, 16 orig: 16, 16 offset: 0, 0 index: -1 ui/icons/icon-save-map rotate: false - xy: 285, 87 + xy: 95, 50 size: 16, 16 orig: 16, 16 offset: 0, 0 index: -1 ui/icons/icon-settings rotate: false - xy: 175, 38 + xy: 820, 418 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 ui/icons/icon-terrain rotate: false - xy: 303, 87 + xy: 592, 243 size: 16, 16 orig: 16, 16 offset: 0, 0 index: -1 ui/icons/icon-tools rotate: false - xy: 997, 467 + xy: 949, 431 size: 14, 14 orig: 14, 14 offset: 0, 0 index: -1 ui/icons/icon-touch rotate: false - xy: 171, 26 + xy: 808, 406 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 ui/icons/icon-touchDelete rotate: false - xy: 171, 14 + xy: 820, 406 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 ui/icons/icon-trash rotate: false - xy: 997, 451 + xy: 965, 447 size: 14, 14 orig: 14, 14 offset: 0, 0 index: -1 ui/icons/icon-trash-16 rotate: false - xy: 321, 87 + xy: 610, 243 size: 16, 16 orig: 16, 16 offset: 0, 0 index: -1 ui/icons/icon-tutorial rotate: false - xy: 175, 50 + xy: 965, 431 size: 14, 14 orig: 14, 14 offset: 0, 0 index: -1 ui/icons/icon-undo rotate: false - xy: 339, 87 + xy: 695, 291 size: 16, 16 orig: 16, 16 offset: 0, 0 index: -1 -ui/icons/icon-units - rotate: false - xy: 781, 432 - size: 10, 10 - orig: 10, 10 - offset: 0, 0 - index: -1 ui/icons/icon-weapon rotate: false - xy: 793, 436 + xy: 832, 410 size: 10, 10 orig: 10, 10 offset: 0, 0 index: -1 ui/icons/icon-zoom rotate: false - xy: 357, 87 + xy: 713, 291 size: 16, 16 orig: 16, 16 offset: 0, 0 index: -1 +ui/icons/icon-zoom-small + rotate: false + xy: 981, 447 + size: 14, 14 + orig: 14, 14 + offset: 0, 0 + index: -1 ui/logotext rotate: false - xy: 143, 108 + xy: 1, 85 size: 89, 21 orig: 89, 21 offset: 0, 0 index: -1 ui/pane rotate: false - xy: 615, 249 + xy: 698, 343 size: 24, 36 split: 10, 10, 5, 5 orig: 24, 36 @@ -2381,7 +2304,7 @@ ui/pane index: -1 ui/pane-button rotate: false - xy: 813, 452 + xy: 495, 140 size: 24, 36 split: 10, 10, 5, 5 orig: 24, 36 @@ -2389,7 +2312,7 @@ ui/pane-button index: -1 ui/scroll rotate: false - xy: 937, 476 + xy: 69, 48 size: 24, 35 split: 10, 10, 6, 5 orig: 24, 35 @@ -2397,7 +2320,7 @@ ui/scroll index: -1 ui/scroll-horizontal rotate: false - xy: 840, 487 + xy: 143, 105 size: 35, 24 split: 6, 5, 10, 10 orig: 35, 24 @@ -2405,7 +2328,7 @@ ui/scroll-horizontal index: -1 ui/scroll-knob-horizontal rotate: false - xy: 495, 131 + xy: 592, 261 size: 40, 24 split: 10, 6, 0, 24 pad: -1, -1, 10, 10 @@ -2414,7 +2337,7 @@ ui/scroll-knob-horizontal index: -1 ui/scroll-knob-vertical rotate: false - xy: 157, 66 + xy: 805, 448 size: 24, 40 split: 10, 10, 6, 10 orig: 24, 40 @@ -2422,7 +2345,7 @@ ui/scroll-knob-vertical index: -1 ui/scroll-knob-vertical-black rotate: false - xy: 787, 448 + xy: 779, 448 size: 24, 40 split: 10, 10, 6, 10 orig: 24, 40 @@ -2430,42 +2353,42 @@ ui/scroll-knob-vertical-black index: -1 ui/selection rotate: false - xy: 537, 132 + xy: 728, 309 size: 1, 1 orig: 1, 1 offset: 0, 0 index: -1 ui/slider rotate: false - xy: 409, 61 + xy: 634, 277 size: 1, 8 orig: 1, 8 offset: 0, 0 index: -1 ui/slider-knob rotate: false - xy: 763, 444 + xy: 495, 178 size: 22, 44 orig: 22, 44 offset: 0, 0 index: -1 ui/slider-knob-down rotate: false - xy: 560, 178 + xy: 698, 381 size: 22, 44 orig: 22, 44 offset: 0, 0 index: -1 ui/slider-knob-over rotate: false - xy: 560, 178 + xy: 698, 381 size: 22, 44 orig: 22, 44 offset: 0, 0 index: -1 ui/slider-vertical rotate: false - xy: 727, 362 + xy: 535, 192 size: 8, 1 orig: 8, 1 offset: 0, 0 @@ -2493,7 +2416,7 @@ ui/text-sides-over index: -1 ui/textfield rotate: false - xy: 993, 483 + xy: 270, 101 size: 28, 28 split: 6, 6, 6, 6 orig: 28, 28 @@ -2501,7 +2424,7 @@ ui/textfield index: -1 ui/textfield-over rotate: false - xy: 963, 483 + xy: 240, 101 size: 28, 28 split: 2, 2, 2, 2 orig: 28, 28 @@ -2509,14 +2432,14 @@ ui/textfield-over index: -1 ui/white rotate: false - xy: 411, 100 + xy: 495, 135 size: 3, 3 orig: 3, 3 offset: 0, 0 index: -1 ui/window rotate: false - xy: 698, 362 + xy: 698, 427 size: 27, 61 split: 8, 8, 44, 11 orig: 27, 61 @@ -2524,7 +2447,7 @@ ui/window index: -1 ui/window-empty rotate: false - xy: 560, 224 + xy: 495, 224 size: 27, 61 split: 8, 8, 44, 11 orig: 27, 61 @@ -2532,84 +2455,84 @@ ui/window-empty index: -1 weapons/beam rotate: false - xy: 221, 62 + xy: 132, 98 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 weapons/beam-equip rotate: false - xy: 411, 87 + xy: 170, 95 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 weapons/blaster rotate: false - xy: 271, 47 + xy: 624, 213 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 weapons/blaster-equip rotate: false - xy: 281, 47 + xy: 634, 235 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 weapons/clustergun rotate: false - xy: 321, 47 + xy: 250, 91 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 weapons/clustergun-equip rotate: false - xy: 331, 49 + xy: 260, 91 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 weapons/shockgun rotate: false - xy: 233, 27 + xy: 542, 110 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 weapons/shockgun-equip rotate: false - xy: 243, 37 + xy: 172, 75 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 weapons/triblaster rotate: false - xy: 333, 19 + xy: 814, 374 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 weapons/triblaster-equip rotate: false - xy: 343, 29 + xy: 794, 344 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 weapons/vulcan rotate: false - xy: 363, 39 + xy: 814, 344 size: 8, 8 orig: 8, 8 offset: 0, 0 index: -1 weapons/vulcan-equip rotate: false - xy: 343, 9 + xy: 748, 332 size: 8, 8 orig: 8, 8 offset: 0, 0 diff --git a/core/assets/sprites/sprites.png b/core/assets/sprites/sprites.png index 10d86fb48c..3eb2c2785e 100644 Binary files a/core/assets/sprites/sprites.png and b/core/assets/sprites/sprites.png differ diff --git a/core/assets/ui/uiskin.json b/core/assets/ui/uiskin.json index 5a83f28256..f6fe02ba53 100644 --- a/core/assets/ui/uiskin.json +++ b/core/assets/ui/uiskin.json @@ -62,7 +62,7 @@ io.anuke.ucore.scene.ui.ImageButton$ImageButtonStyle: { emptytoggle: {imageCheckedColor: white, imageDownColor: white, imageUpColor: lightgray}, static: {up: button }, static-down: {up: button-down }, - toggle: {checked: button-down, down: button-down, up: button }, + toggle: {checked: button-down, down: button-down, up: button, imageDisabledColor: lightgray, imageUpColor: white }, togglemap: {down: button-map-down, up: button-map }, select: {checked: button-select, up: clear }, close-window: {up: button, imageUp: icon-close, imageOver: icon-close-over, imageDown: icon-close-down, disabled: button } @@ -83,7 +83,7 @@ io.anuke.ucore.scene.ui.Window$WindowStyle: { dialog: {stageBackground: dialogDim, titleFont: default-font, background: window-empty, titleFontColor: accent } }, io.anuke.ucore.scene.ui.KeybindDialog$KeybindDialogStyle: { - default: {keyColor: accent, keyNameColor: white, paneStyle: clear}, + default: {keyColor: accent, keyNameColor: white, controllerColor: menuitem, paneStyle: clear}, }, io.anuke.ucore.scene.ui.Slider$SliderStyle: { default-horizontal: {background: slider, knob: slider-knob, knobOver: slider-knob-over, knobDown: slider-knob-down}, diff --git a/core/assets/version.properties b/core/assets/version.properties index f1ec39dc98..a98e97f755 100644 --- a/core/assets/version.properties +++ b/core/assets/version.properties @@ -1,7 +1,7 @@ #Autogenerated file. Do not modify. -#Thu Feb 22 23:52:21 EST 2018 +#Tue Feb 27 19:35:27 EST 2018 version=release -androidBuildCode=282 +androidBuildCode=313 name=Mindustry -code=3.3 -build=custom build +code=3.4 +build=29 diff --git a/core/src/Mindustry.gwt.xml b/core/src/Mindustry.gwt.xml index 5fdf4b2778..c964b6179d 100644 --- a/core/src/Mindustry.gwt.xml +++ b/core/src/Mindustry.gwt.xml @@ -15,4 +15,5 @@ + \ No newline at end of file diff --git a/core/src/io/anuke/mindustry/Vars.java b/core/src/io/anuke/mindustry/Vars.java index 73c10dbab9..cb5f7584ae 100644 --- a/core/src/io/anuke/mindustry/Vars.java +++ b/core/src/io/anuke/mindustry/Vars.java @@ -93,7 +93,7 @@ public class Vars{ public static final int tilesize = 8; public static final Locale[] locales = {new Locale("en"), new Locale("fr", "FR"), new Locale("ru"), new Locale("pl", "PL"), - new Locale("es", "LA"), new Locale("pt", "BR"), new Locale("ko"), new Locale("in", "ID")}; + new Locale("de"), new Locale("es", "LA"), new Locale("pt", "BR"), new Locale("ko"), new Locale("in", "ID")}; public static final Color[] playerColors = { Color.valueOf("82759a"), diff --git a/core/src/io/anuke/mindustry/ai/Pathfind.java b/core/src/io/anuke/mindustry/ai/Pathfind.java index 5099bd2df1..1d65921692 100644 --- a/core/src/io/anuke/mindustry/ai/Pathfind.java +++ b/core/src/io/anuke/mindustry/ai/Pathfind.java @@ -169,8 +169,8 @@ public class Pathfind{ /**Reset and clear the paths.*/ public void resetPaths(){ - for(SpawnPoint point : world.getSpawns()){ - resetPathFor(point); + for(int i = 0; i < world.getSpawns().size; i ++){ + resetPathFor(world.getSpawns().get(i)); } } diff --git a/core/src/io/anuke/mindustry/core/Logic.java b/core/src/io/anuke/mindustry/core/Logic.java index 87e3581e2f..640a30788e 100644 --- a/core/src/io/anuke/mindustry/core/Logic.java +++ b/core/src/io/anuke/mindustry/core/Logic.java @@ -121,7 +121,7 @@ public class Logic extends Module { if(world.getCore() != null && world.getCore().block() != ProductionBlocks.core && !state.gameOver){ state.gameOver = true; - NetEvents.handleGameOver(); + if(Net.server()) NetEvents.handleGameOver(); Events.fire(GameOverEvent.class); } diff --git a/core/src/io/anuke/mindustry/core/NetClient.java b/core/src/io/anuke/mindustry/core/NetClient.java index 97d037ec44..cbfa80445a 100644 --- a/core/src/io/anuke/mindustry/core/NetClient.java +++ b/core/src/io/anuke/mindustry/core/NetClient.java @@ -17,7 +17,9 @@ 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.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.Timers; @@ -49,6 +51,7 @@ public class NetClient extends Module { public NetClient(){ Net.handleClient(Connect.class, packet -> { + player.isAdmin = false; Net.setClientLoaded(false); recieved.clear(); @@ -159,12 +162,22 @@ public class NetClient extends Module { state.wavetime = packet.countdown; state.wave = packet.wave; - //removed: messing with time isn't necessary anymore - //Timers.resetTime(packet.time + (float) (TimeUtils.timeSinceMillis(packet.timestamp) / 1000.0 * 60.0)); - ui.hudfrag.updateItems(); }); + Net.handleClient(PlacePacket.class, (packet) -> { + Placement.placeBlock(packet.x, packet.y, Block.getByID(packet.block), packet.rotation, true, false); + + 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, false); + }); + Net.handleClient(EntitySpawnPacket.class, packet -> { EntityGroup group = packet.group; @@ -316,6 +329,22 @@ public class NetClient extends Module { r.run(); } }); + + 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); + }); } @Override @@ -329,12 +358,6 @@ public class NetClient extends Module { } } - //TODO remove. - public void test(){ - gotData = false; - connecting = true; - } - public boolean hasData(){ return gotData; } diff --git a/core/src/io/anuke/mindustry/core/NetCommon.java b/core/src/io/anuke/mindustry/core/NetCommon.java index b2326211cd..9b8a9435da 100644 --- a/core/src/io/anuke/mindustry/core/NetCommon.java +++ b/core/src/io/anuke/mindustry/core/NetCommon.java @@ -4,11 +4,8 @@ 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.Recipe; -import io.anuke.mindustry.resource.Recipes; import io.anuke.mindustry.resource.Upgrade; import io.anuke.mindustry.resource.Weapon; -import io.anuke.mindustry.world.Block; import io.anuke.mindustry.world.Tile; import io.anuke.ucore.modules.Module; @@ -25,23 +22,6 @@ public class NetCommon extends Module { weapon.shoot(player, packet.x, packet.y, packet.rotation); }); - Net.handle(PlacePacket.class, (packet) -> { - if(headless) - world.placeBlock(packet.x, packet.y, Block.getByID(packet.block), packet.rotation); - else - control.input().placeBlockInternal(packet.x, packet.y, Block.getByID(packet.block), packet.rotation, true, false); - - Recipe recipe = Recipes.getByResult(Block.getByID(packet.block)); - if (recipe != null) state.inventory.removeItems(recipe.requirements); - }); - - Net.handle(BreakPacket.class, (packet) -> { - if(headless) - world.removeBlock(world.tile(packet.x, packet.y)); - else - control.input().breakBlockInternal(packet.x, packet.y, false); - }); - Net.handle(ChatPacket.class, (packet) -> { ui.chatfrag.addMessage(packet.text, colorizeName(packet.id, packet.name)); }); diff --git a/core/src/io/anuke/mindustry/core/NetServer.java b/core/src/io/anuke/mindustry/core/NetServer.java index ef5e91e131..5c7758e053 100644 --- a/core/src/io/anuke/mindustry/core/NetServer.java +++ b/core/src/io/anuke/mindustry/core/NetServer.java @@ -7,14 +7,15 @@ import io.anuke.mindustry.entities.SyncEntity; import io.anuke.mindustry.game.EventType.GameOverEvent; import io.anuke.mindustry.io.Platform; import io.anuke.mindustry.io.Version; +import io.anuke.mindustry.net.Administration; import io.anuke.mindustry.net.Net; import io.anuke.mindustry.net.Net.SendMode; import io.anuke.mindustry.net.NetConnection; import io.anuke.mindustry.net.NetworkIO; import io.anuke.mindustry.net.Packets.*; -import io.anuke.mindustry.resource.Upgrade; -import io.anuke.mindustry.resource.UpgradeRecipes; -import io.anuke.mindustry.resource.Weapon; +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; @@ -41,6 +42,8 @@ public class NetServer extends Module{ private final static int timerStateSync = 1; private final static int timerBlockSync = 2; + public final Administration admins = new Administration(); + /**Maps connection IDs to players.*/ private IntMap connections = new IntMap<>(); private ObjectMap weapons = new ObjectMap<>(); @@ -51,18 +54,33 @@ public class NetServer extends Module{ Events.on(GameOverEvent.class, () -> weapons.clear()); - Net.handleServer(Connect.class, (id, connect) -> {}); + Net.handleServer(Connect.class, (id, connect) -> { + if(admins.isBanned(connect.addressTCP)){ + Net.kickConnection(id, KickReason.banned); + } + }); Net.handleServer(ConnectPacket.class, (id, packet) -> { + if(Net.getConnection(id) == null || + admins.isBanned(Net.getConnection(id).address)) return; - if(packet.version != Version.build && packet.version != -1 && Version.build != -1){ //ignore 'custom builds' on both ends + String ip = Net.getConnection(id).address; + + admins.setKnownName(ip, packet.name); + + if(packet.version != Version.build && Version.build != -1 && packet.version != -1){ Net.kickConnection(id, packet.version > Version.build ? KickReason.serverOutdated : KickReason.clientOutdated); return; } + if(packet.version == -1){ + admins.getTrace(ip).modclient = true; + } + Log.info("Sending data to player '{0}' / {1}", packet.name, id); Player player = new Player(); + player.isAdmin = admins.isAdmin(Net.getConnection(id).address); player.clientid = id; player.name = packet.name; player.isAndroid = packet.android; @@ -72,6 +90,8 @@ public class NetServer extends Module{ player.color.set(packet.color); connections.put(id, player); + admins.getTrace(ip).playerid = player.id; + if(world.getMap().custom){ ByteArrayOutputStream stream = new ByteArrayOutputStream(); NetworkIO.writeMap(world.getMap(), stream); @@ -92,7 +112,7 @@ public class NetServer extends Module{ Player player = connections.get(id); ByteArrayOutputStream stream = new ByteArrayOutputStream(); - NetworkIO.writeWorld(player.id, weapons.get(player.name, new ByteArray()), stream); + NetworkIO.writeWorld(player, weapons.get(player.name, new ByteArray()), stream); WorldData data = new WorldData(); data.stream = new ByteArrayInputStream(stream.toByteArray()); Net.sendStream(id, data); @@ -145,19 +165,53 @@ public class NetServer extends Module{ Net.handleServer(PlacePacket.class, (id, packet) -> { packet.playerid = connections.get(id).id; - Net.sendExcept(id, packet, SendMode.tcp); + + Block block = Block.getByID(packet.block); + + if(!Placement.validPlace(packet.x, packet.y, block)) return; + + Recipe recipe = Recipes.getByResult(block); + + if(recipe == null) return; + + state.inventory.removeItems(recipe.requirements); + + Placement.placeBlock(packet.x, packet.y, block, packet.rotation, true, false); + + admins.getTrace(Net.getConnection(id).address).lastBlockPlaced = block; + admins.getTrace(Net.getConnection(id).address).totalBlocksPlaced ++; + + Net.send(packet, SendMode.tcp); }); Net.handleServer(BreakPacket.class, (id, packet) -> { packet.playerid = connections.get(id).id; - Net.sendExcept(id, packet, SendMode.tcp); + + if(!Placement.validBreak(packet.x, packet.y)) return; + + Block block = Placement.breakBlock(packet.x, packet.y, true, false); + + if(block != null) { + admins.getTrace(Net.getConnection(id).address).lastBlockBroken = block; + admins.getTrace(Net.getConnection(id).address).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.sendExcept(player.clientid, packet, SendMode.tcp); + Net.send(packet, SendMode.tcp); }); Net.handleServer(UpgradePacket.class, (id, packet) -> { @@ -200,6 +254,39 @@ public class NetServer extends Module{ 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.banPlayer(ip); + Net.kickConnection(other.clientid, KickReason.banned); + Log.info("&lc{0} has banned {1}.", player.name, other.name); + }else if(packet.action == AdminAction.kick){ + Net.kickConnection(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); + } + }); } public void update(){ @@ -221,6 +308,7 @@ public class NetServer extends Module{ public void reset(){ weapons.clear(); + admins.clearTraces(); } void sync(){ diff --git a/core/src/io/anuke/mindustry/core/Renderer.java b/core/src/io/anuke/mindustry/core/Renderer.java index 7581ba778d..ba37bc6e2b 100644 --- a/core/src/io/anuke/mindustry/core/Renderer.java +++ b/core/src/io/anuke/mindustry/core/Renderer.java @@ -264,8 +264,14 @@ public class Renderer extends RendererModule{ Draw.color(); Draw.tcolor(player.getColor()); Draw.text(player.name, player.x, player.y + 8); - Draw.tcolor(); - } + + if(player.isAdmin){ + Draw.color(player.getColor()); + float s = 3f; + Draw.rect("icon-admin-small", player.x + layout.width/2f + 2 + 1, player.y + 7f, s, s); + } + Draw.reset(); + } } Pools.free(layout); Draw.tscl(fontscale); @@ -274,6 +280,7 @@ public class Renderer extends RendererModule{ void drawEnemyMarkers(){ Graphics.surface(indicatorSurface); Draw.color(Color.RED); + for(Enemy enemy : enemyGroup.all()) { if (rect.setSize(camera.viewportWidth, camera.viewportHeight).setCenter(camera.position.x, camera.position.y) @@ -409,8 +416,10 @@ public class Renderer extends RendererModule{ Lines.dashCircle(spawn.start.worldx(), spawn.start.worldy(), enemyspawnspace); } - Draw.color(Color.LIME); - Lines.poly(world.getSpawnX(), world.getSpawnY(), 4, 6f, Timers.time()*2f); + 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); diff --git a/core/src/io/anuke/mindustry/core/ThreadHandler.java b/core/src/io/anuke/mindustry/core/ThreadHandler.java index b4f08fc703..c3823cc692 100644 --- a/core/src/io/anuke/mindustry/core/ThreadHandler.java +++ b/core/src/io/anuke/mindustry/core/ThreadHandler.java @@ -27,7 +27,10 @@ public class ThreadHandler { public ThreadHandler(ThreadProvider impl){ this.impl = impl; - Timers.setDeltaProvider(() -> impl.isOnThread() ? delta : Gdx.graphics.getDeltaTime()*60f); + Timers.setDeltaProvider(() ->{ + float result = impl.isOnThread() ? delta : Gdx.graphics.getDeltaTime()*60f; + return Float.isNaN(result) ? 1f : result; + }); } public void run(Runnable r){ diff --git a/core/src/io/anuke/mindustry/core/UI.java b/core/src/io/anuke/mindustry/core/UI.java index 85483571e3..94d0559720 100644 --- a/core/src/io/anuke/mindustry/core/UI.java +++ b/core/src/io/anuke/mindustry/core/UI.java @@ -45,6 +45,9 @@ public class UI extends SceneModule{ public ControlsDialog controls; public MapEditorDialog editor; public LanguageDialog language; + public BansDialog bans; + public AdminsDialog admins; + public TraceDialog traces; public final MenuFragment menufrag = new MenuFragment(); public final ToolFragment toolfrag = new ToolFragment(); @@ -150,6 +153,9 @@ public class UI extends SceneModule{ paused = new PausedDialog(); about = new AboutDialog(); host = new HostDialog(); + bans = new BansDialog(); + admins = new AdminsDialog(); + traces = new TraceDialog(); build.begin(scene); diff --git a/core/src/io/anuke/mindustry/core/World.java b/core/src/io/anuke/mindustry/core/World.java index f9506a7b2a..ac75f0f0a4 100644 --- a/core/src/io/anuke/mindustry/core/World.java +++ b/core/src/io/anuke/mindustry/core/World.java @@ -8,10 +8,7 @@ import io.anuke.mindustry.ai.Pathfind; import io.anuke.mindustry.entities.TileEntity; import io.anuke.mindustry.game.SpawnPoint; import io.anuke.mindustry.io.Maps; -import io.anuke.mindustry.world.Block; -import io.anuke.mindustry.world.Map; -import io.anuke.mindustry.world.Tile; -import io.anuke.mindustry.world.WorldGenerator; +import io.anuke.mindustry.world.*; import io.anuke.mindustry.world.blocks.Blocks; import io.anuke.mindustry.world.blocks.DistributionBlocks; import io.anuke.mindustry.world.blocks.ProductionBlocks; @@ -22,7 +19,8 @@ import io.anuke.ucore.modules.Module; import io.anuke.ucore.util.Mathf; import io.anuke.ucore.util.Tmp; -import static io.anuke.mindustry.Vars.*; +import static io.anuke.mindustry.Vars.control; +import static io.anuke.mindustry.Vars.tilesize; public class World extends Module{ private int seed; @@ -182,7 +180,7 @@ public class World extends Module{ core = WorldGenerator.generate(map.pixmap, tiles, spawns); - placeBlock(core.x, core.y, ProductionBlocks.core, 0); + Placement.placeBlock(core.x, core.y, ProductionBlocks.core, 0, false, false); if(!map.name.equals("tutorial")){ setDefaultBlocks(); @@ -233,7 +231,7 @@ public class World extends Module{ public int getSeed(){ return seed; } - + public void removeBlock(Tile tile){ if(!tile.block().isMultiblock() && !tile.isLinked()){ tile.setBlock(Blocks.air); @@ -246,32 +244,6 @@ public class World extends Module{ } } } - - public void placeBlock(int x, int y, Block result, int rotation){ - Tile tile = world.tile(x, y); - - //just in case - if(tile == null) return; - - tile.setBlock(result, rotation); - - if(result.isMultiblock()){ - int offsetx = -(result.width-1)/2; - int offsety = -(result.height-1)/2; - - for(int dx = 0; dx < result.width; dx ++){ - for(int dy = 0; dy < result.height; dy ++){ - int worldx = dx + offsetx + x; - int worldy = dy + offsety + y; - if(!(worldx == x && worldy == y)){ - Tile toplace = world.tile(worldx, worldy); - if(toplace != null) - toplace.setLinked((byte)(dx + offsetx), (byte)(dy + offsety)); - } - } - } - } - } public TileEntity findTileTarget(float x, float y, Tile tile, float range, boolean damaged){ Entity closest = null; diff --git a/core/src/io/anuke/mindustry/entities/Player.java b/core/src/io/anuke/mindustry/entities/Player.java index 86a28df1eb..5ef9259598 100644 --- a/core/src/io/anuke/mindustry/entities/Player.java +++ b/core/src/io/anuke/mindustry/entities/Player.java @@ -34,6 +34,7 @@ public class Player extends SyncEntity{ public String name = "name"; public boolean isAndroid; + public boolean isAdmin; public Color color = new Color(); public Weapon weaponLeft = Weapon.blaster; @@ -44,7 +45,7 @@ public class Player extends SyncEntity{ public float stucktime = 0f; public boolean dashing = false; - public int clientid; + public int clientid = -1; public boolean isLocal = false; public Timer timer = new Timer(4); @@ -83,7 +84,7 @@ public class Player extends SyncEntity{ @Override public void onDeath(){ - remove(); + dead = true; if(Net.active()){ NetEvents.handlePlayerDeath(); } @@ -112,7 +113,7 @@ public class Player extends SyncEntity{ @Override public void drawSmooth(){ - if((debug && (!showPlayer || !showUI)) || (isAndroid && isLocal) || (dead && !isLocal)) return; + if((debug && (!showPlayer || !showUI)) || (isAndroid && isLocal) || dead) return; boolean snap = snapCamera && Settings.getBool("smoothcam") && Settings.getBool("pixelate") && isLocal; String part = isAndroid ? "ship" : "mech"; @@ -155,6 +156,8 @@ public class Player extends SyncEntity{ return; } + if(isDead()) return; + Tile tile = world.tileWorld(x, y); //if player is in solid block @@ -240,6 +243,7 @@ public class Player extends SyncEntity{ buffer.put(weaponLeft.id); buffer.put(weaponRight.id); buffer.put(isAndroid ? 1 : (byte)0); + buffer.put(isAdmin ? 1 : (byte)0); buffer.putInt(Color.rgba8888(color)); buffer.putFloat(x); buffer.putFloat(y); @@ -254,6 +258,7 @@ public class Player extends SyncEntity{ weaponLeft = (Weapon) Upgrade.getByID(buffer.get()); weaponRight = (Weapon) Upgrade.getByID(buffer.get()); isAndroid = buffer.get() == 1; + isAdmin = buffer.get() == 1; color.set(buffer.getInt()); x = buffer.getFloat(); y = buffer.getFloat(); diff --git a/core/src/io/anuke/mindustry/entities/enemies/EnemyType.java b/core/src/io/anuke/mindustry/entities/enemies/EnemyType.java index 27f0f4bd99..9ce29dadea 100644 --- a/core/src/io/anuke/mindustry/entities/enemies/EnemyType.java +++ b/core/src/io/anuke/mindustry/entities/enemies/EnemyType.java @@ -17,6 +17,7 @@ import io.anuke.ucore.core.Graphics; import io.anuke.ucore.core.Timers; import io.anuke.ucore.entities.Entities; import io.anuke.ucore.graphics.Draw; +import io.anuke.ucore.graphics.Lines; import io.anuke.ucore.util.Mathf; import io.anuke.ucore.util.Strings; @@ -82,6 +83,13 @@ public class EnemyType { Graphics.flush(); + if(isCalculating(enemy)){ + Draw.color(Color.SKY); + Lines.polySeg(20, 0, 4, enemy.x, enemy.y, 11f, Timers.time() * 2f + enemy.id*52f); + Lines.polySeg(20, 0, 4, enemy.x, enemy.y, 11f, Timers.time() * 2f + enemy.id*52f + 180f); + Draw.color(); + } + if(showPaths){ Draw.tscl(0.25f); Draw.text((int)enemy.idletime + " " + enemy.node + " " + enemy.id + "\n" + Strings.toFixed(enemy.totalMove.x, 2) + ", " @@ -100,9 +108,10 @@ public class EnemyType { enemy.hitTime -= Timers.delta(); } - if(enemy.lane >= world.getSpawns().size) enemy.lane = 0; + if(enemy.lane >= world.getSpawns().size || enemy.lane < 0) enemy.lane = 0; - boolean waiting = world.getSpawns().get(enemy.lane).pathTiles == null || enemy.node <= 0; + boolean waiting = enemy.lane >= world.getSpawns().size || enemy.lane < 0 + || world.getSpawns().get(enemy.lane).pathTiles == null || enemy.node <= 0; move(enemy); @@ -156,6 +165,8 @@ public class EnemyType { Tile core = world.getCore(); + if(core == null) return; + if(enemy.idletime > maxIdleLife && enemy.node > 0){ enemy.onDeath(); return; @@ -189,7 +200,7 @@ public class EnemyType { }else if(dst < avoidRange){ calc.set((enemy.x - other.x), (enemy.y - other.y)).setLength(avoidSpeed); shift.add(calc.scl(1.1f)); - }else if(dst < attractRange && !nearCore){ + }else if(dst < attractRange && !nearCore && !isCalculating(enemy)){ calc.set((enemy.x - other.x), (enemy.y - other.y)).setLength(avoidSpeed); shift.add(calc.scl(-1)); } @@ -217,7 +228,8 @@ public class EnemyType { //no tile found if(enemy.target == null){ - enemy.target = Entities.getClosest(playerGroup, enemy.x, enemy.y, range, e -> !((Player)e).isAndroid); + enemy.target = Entities.getClosest(playerGroup, enemy.x, enemy.y, range, e -> !((Player)e).isAndroid && + !((Player)e).isDead()); } }else if(nearCore){ enemy.target = world.getCore().entity; @@ -267,6 +279,10 @@ public class EnemyType { } } + public boolean isCalculating(Enemy enemy){ + return enemy.node < 0 && !Net.client(); + } + public static EnemyType getByID(byte id){ return types.get(id); } diff --git a/core/src/io/anuke/mindustry/entities/enemies/types/TargetType.java b/core/src/io/anuke/mindustry/entities/enemies/types/TargetType.java index b8e98a7b1e..822e9d5823 100644 --- a/core/src/io/anuke/mindustry/entities/enemies/types/TargetType.java +++ b/core/src/io/anuke/mindustry/entities/enemies/types/TargetType.java @@ -56,4 +56,9 @@ public class TargetType extends EnemyType { new Enemy(EnemyTypes.target).set(enemy.x, enemy.y).add(); }); } + + @Override + public boolean isCalculating(Enemy enemy){ + return false; + } } diff --git a/core/src/io/anuke/mindustry/graphics/BlockRenderer.java b/core/src/io/anuke/mindustry/graphics/BlockRenderer.java index 7a610a5518..e53ffacd1b 100644 --- a/core/src/io/anuke/mindustry/graphics/BlockRenderer.java +++ b/core/src/io/anuke/mindustry/graphics/BlockRenderer.java @@ -177,7 +177,7 @@ public class BlockRenderer{ OrthographicCamera camera = Core.camera; - Graphics.end(); + if(Graphics.drawing()) Graphics.end(); int crangex = (int)(camera.viewportWidth * camera.zoom / (chunksize * tilesize))+1; int crangey = (int)(camera.viewportHeight * camera.zoom / (chunksize * tilesize))+1; diff --git a/core/src/io/anuke/mindustry/input/InputHandler.java b/core/src/io/anuke/mindustry/input/InputHandler.java index ae0a9c64f9..88ef529f4e 100644 --- a/core/src/io/anuke/mindustry/input/InputHandler.java +++ b/core/src/io/anuke/mindustry/input/InputHandler.java @@ -2,26 +2,16 @@ package io.anuke.mindustry.input; import com.badlogic.gdx.InputAdapter; import com.badlogic.gdx.math.GridPoint2; -import com.badlogic.gdx.math.Rectangle; import com.badlogic.gdx.math.Vector2; -import com.badlogic.gdx.utils.Array; -import io.anuke.mindustry.entities.Player; -import io.anuke.mindustry.game.SpawnPoint; -import io.anuke.mindustry.graphics.Fx; import io.anuke.mindustry.net.Net; import io.anuke.mindustry.net.NetEvents; import io.anuke.mindustry.resource.ItemStack; import io.anuke.mindustry.resource.Recipe; -import io.anuke.mindustry.resource.Recipes; import io.anuke.mindustry.world.Block; +import io.anuke.mindustry.world.Placement; 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.Effects; import io.anuke.ucore.core.Graphics; -import io.anuke.ucore.core.Sounds; -import io.anuke.ucore.entities.Entities; -import io.anuke.ucore.entities.SolidEntity; import io.anuke.ucore.util.Mathf; import static io.anuke.mindustry.Vars.*; @@ -35,8 +25,6 @@ public abstract class InputHandler extends InputAdapter{ public PlaceMode lastPlaceMode = placeMode; public PlaceMode lastBreakMode = breakMode; - private Rectangle rect = new Rectangle(); - public abstract void update(); public abstract float getCursorX(); public abstract float getCursorY(); @@ -88,39 +76,6 @@ public abstract class InputHandler extends InputAdapter{ public boolean validPlace(int x, int y, Block type){ - for(SpawnPoint spawn : world.getSpawns()){ - if(Vector2.dst(x * tilesize, y * tilesize, spawn.start.worldx(), spawn.start.worldy()) < enemyspawnspace){ - return false; - } - } - - rect.setSize(type.width * tilesize, type.height * tilesize); - Vector2 offset = type.getPlaceOffset(); - rect.setCenter(offset.x + x * tilesize, offset.y + y * tilesize); - - synchronized (Entities.entityLock) { - for (SolidEntity e : Entities.getNearby(enemyGroup, x * tilesize, y * tilesize, tilesize * 2f)) { - if (e == null) continue; //not sure why this happens? - Rectangle rect = e.hitbox.getRect(e.x, e.y); - - if (this.rect.overlaps(rect)) { - return false; - } - } - } - - if(type.solid || type.solidifes) { - for (Player player : playerGroup.all()) { - if (!player.isAndroid && rect.overlaps(player.hitbox.getRect(player.x, player.y))) { - return false; - } - } - } - - Tile tile = world.tile(x, y); - - if(tile == null || (isSpawnPoint(tile) && (type.solidifes || type.solid))) return false; - if(!type.isMultiblock() && control.tutorial().active() && control.tutorial().showBlock()){ @@ -135,40 +90,11 @@ public abstract class InputHandler extends InputAdapter{ }else if(control.tutorial().active()){ return false; } - - if(type.isMultiblock()){ - int offsetx = -(type.width-1)/2; - int offsety = -(type.height-1)/2; - for(int dx = 0; dx < type.width; dx ++){ - for(int dy = 0; dy < type.height; dy ++){ - Tile other = world.tile(x + dx + offsetx, y + dy + offsety); - if(other == null || (other.block() != Blocks.air && !other.block().alwaysReplace) || isSpawnPoint(other)){ - return false; - } - } - } - return true; - }else{ - if(tile.block() != type && (type.canReplace(tile.block()) || tile.block().alwaysReplace) && tile.block().isMultiblock() == type.isMultiblock()){ - return true; - } - return tile.block() == Blocks.air; - } - } - public boolean isSpawnPoint(Tile tile){ - return tile != null && tile.x == world.getCore().x && tile.y == world.getCore().y - 2; + return Placement.validPlace(x, y, type); } public boolean validBreak(int x, int y){ - Tile tile = world.tile(x, y); - - if(tile == null || tile.block() == ProductionBlocks.core) return false; - - if(tile.isLinked() && tile.getLinked().block() == ProductionBlocks.core){ - return false; - } - if(control.tutorial().active()){ if(control.tutorial().showBlock()){ @@ -185,102 +111,26 @@ public abstract class InputHandler extends InputAdapter{ } } - return tile.breakable(); + return Placement.validBreak(x, y); } public void placeBlock(int x, int y, Block result, int rotation, boolean effects, boolean sound){ + if(!Net.client()){ + Placement.placeBlock(x, y, result, rotation, effects, sound); + Tile tile = world.tile(x, y); + if(tile != null) result.placed(tile); + } - placeBlockInternal(x, y, result, rotation, effects, sound); - - Tile tile = world.tile(x, y); - - if(tile != null) result.placed(tile); - - if(Net.active() && result != ProductionBlocks.core){ + if(Net.active()){ NetEvents.handlePlace(x, y, result, rotation); } } - public void placeBlockInternal(int x, int y, Block result, int rotation, boolean effects, boolean sound){ - Tile tile = world.tile(x, y); - - //just in case - if(tile == null) - return; - - tile.setBlock(result, rotation); - - if(result.isMultiblock()){ - int offsetx = -(result.width-1)/2; - int offsety = -(result.height-1)/2; - - for(int dx = 0; dx < result.width; dx ++){ - for(int dy = 0; dy < result.height; dy ++){ - int worldx = dx + offsetx + x; - int worldy = dy + offsety + y; - if(!(worldx == x && worldy == y)){ - Tile toplace = world.tile(worldx, worldy); - if(toplace != null) - toplace.setLinked((byte)(dx + offsetx), (byte)(dy + offsety)); - } - - if(effects) Effects.effect(Fx.place, worldx * tilesize, worldy * tilesize); - } - } - }else{ - if(effects) Effects.effect(Fx.place, x * tilesize, y * tilesize); - } - - if(effects && sound) Sounds.play("place"); - } - public void breakBlock(int x, int y, boolean sound){ - breakBlockInternal(x, y, sound); + if(!Net.client()) Placement.breakBlock(x, y, true, sound); if(Net.active()){ NetEvents.handleBreak(x, y); } } - - public void breakBlockInternal(int x, int y, boolean sound){ - Tile tile = world.tile(x, y); - - if(tile == null) return; - - Block block = tile.isLinked() ? tile.getLinked().block() : tile.block(); - Recipe result = null; - - for(Recipe recipe : Recipes.all()){ - if(recipe.result == block){ - result = recipe; - break; - } - } - - if(result != null){ - for(ItemStack stack : result.requirements){ - state.inventory.addItem(stack.item, (int)(stack.amount * breakDropAmount)); - } - } - - if(tile.block().drops != null){ - state.inventory.addItem(tile.block().drops.item, tile.block().drops.amount); - } - - //Effects.shake(3f, 1f, player); - if(sound) Sounds.play("break"); - - if(!tile.block().isMultiblock() && !tile.isLinked()){ - tile.setBlock(Blocks.air); - Effects.effect(Fx.breakBlock, tile.worldx(), tile.worldy()); - }else{ - Tile target = tile.isLinked() ? tile.getLinked() : tile; - Array removals = target.getLinkedTiles(); - for(Tile toremove : removals){ - //note that setting a new block automatically unlinks it - toremove.setBlock(Blocks.air); - Effects.effect(Fx.breakBlock, toremove.worldx(), toremove.worldy()); - } - } - } } diff --git a/core/src/io/anuke/mindustry/io/Maps.java b/core/src/io/anuke/mindustry/io/Maps.java index 7d583a6174..ad41c3f431 100644 --- a/core/src/io/anuke/mindustry/io/Maps.java +++ b/core/src/io/anuke/mindustry/io/Maps.java @@ -75,13 +75,14 @@ public class Maps implements Disposable{ } public void loadMaps(){ - if(!loadMapFile(Gdx.files.internal("maps/maps.json"))){ + if(!loadMapFile(Gdx.files.internal("maps/maps.json"), true)){ throw new RuntimeException("Failed to load maps!"); } if(!gwt) { - if (!loadMapFile(customMapDirectory.child("maps.json"))) { + if (!loadMapFile(customMapDirectory.child("maps.json"), false)) { try { + Log.info("Failed to find custom map directory."); customMapDirectory.child("maps.json").writeString("{}", false); } catch (Exception e) { throw new RuntimeException("Failed to create custom map directory!"); @@ -159,25 +160,30 @@ public class Maps implements Disposable{ saveMaps(out, customMapDirectory.child("maps.json")); } - private boolean loadMapFile(FileHandle file){ - try{ + private boolean loadMapFile(FileHandle file, boolean logException){ + try { Array arr = json.fromJson(ArrayContainer.class, file).maps; - if(arr != null){ //can be an empty map file - for(Map map : arr){ + if (arr != null) { //can be an empty map file + for (Map map : arr) { map.pixmap = new Pixmap(file.sibling(map.name + ".png")); - if(!headless) map.texture = new Texture(map.pixmap); + if (!headless) map.texture = new Texture(map.pixmap); maps.put(map.id, map); mapNames.put(map.name, map); lastID = Math.max(lastID, map.id); - if(!map.custom){ + if (!map.custom) { defaultMaps.add(map); } } } return true; - }catch(Exception e){ + }catch (GdxRuntimeException e){ Log.err(e); - Log.err("Failed loading map file: {0}", file); + return true; + }catch(Exception e){ + if(logException) { + Log.err(e); + Log.err("Failed loading map file: {0}", file); + } return false; } } diff --git a/core/src/io/anuke/mindustry/io/versions/Save15.java b/core/src/io/anuke/mindustry/io/versions/Save15.java index df955efa49..cd654ba7de 100644 --- a/core/src/io/anuke/mindustry/io/versions/Save15.java +++ b/core/src/io/anuke/mindustry/io/versions/Save15.java @@ -307,7 +307,7 @@ public class Save15 extends SaveFileVersion { for(int y = 0; y < world.height(); y ++){ Tile tile = world.tile(x, y); - if(tile.breakable()){ + if(tile != null && tile.breakable()){ if(tile.block() instanceof Rock){ totalrocks ++; }else{ @@ -325,7 +325,7 @@ public class Save15 extends SaveFileVersion { for (int y = 0; y < world.height(); y++) { Tile tile = world.tile(x, y); - if (tile.block() instanceof Rock) { + if (tile != null && tile.block() instanceof Rock) { stream.writeInt(tile.packedPosition()); } } @@ -338,7 +338,7 @@ public class Save15 extends SaveFileVersion { for(int y = 0; y < world.height(); y ++){ Tile tile = world.tile(x, y); - if(tile.breakable() && !(tile.block() instanceof Rock)){ + if(tile != null && tile.breakable() && !(tile.block() instanceof Rock)){ stream.writeInt(x + y*world.width()); //tile pos stream.writeInt(tile.block().id); //block ID diff --git a/core/src/io/anuke/mindustry/net/Administration.java b/core/src/io/anuke/mindustry/net/Administration.java new file mode 100644 index 0000000000..27ebe8f1d5 --- /dev/null +++ b/core/src/io/anuke/mindustry/net/Administration.java @@ -0,0 +1,125 @@ +package io.anuke.mindustry.net; + +import com.badlogic.gdx.utils.Array; +import com.badlogic.gdx.utils.Json; +import com.badlogic.gdx.utils.ObjectMap; +import io.anuke.ucore.core.Settings; + +public class Administration { + private Json json = new Json(); + private Array bannedIPS = new Array<>(); + private Array admins = new Array<>(); + private ObjectMap known = new ObjectMap<>(); + private ObjectMap traces = new ObjectMap<>(); + + public Administration(){ + Settings.defaultList( + "bans", "{}", + "admins", "{}", + "knownIPs", "{}" + ); + + load(); + } + + public TraceInfo getTrace(String ip){ + if(!traces.containsKey(ip)) traces.put(ip, new TraceInfo(ip)); + + return traces.get(ip); + } + + public void clearTraces(){ + traces.clear(); + } + + /**Sets last known name for an IP.*/ + public void setKnownName(String ip, String name){ + known.put(ip, name); + saveKnown(); + } + + /**Returns the last known name for an IP. Returns 'unknown' if this IP has an unknown username.*/ + public String getLastName(String ip){ + return known.get(ip, "unknown"); + } + + /**Returns list of banned IPs.*/ + public Array getBanned(){ + return bannedIPS; + } + + /**Bans a player by IP; returns whether this player was already banned.*/ + public boolean banPlayer(String ip){ + if(bannedIPS.contains(ip, false)) + return false; + bannedIPS.add(ip); + saveBans(); + + return true; + } + + /**Unbans a player by IP; returns whether this player was banned in the first place..*/ + public boolean unbanPlayer(String ip){ + if(!bannedIPS.contains(ip, false)) + return false; + bannedIPS.removeValue(ip, false); + saveBans(); + + return true; + } + + /**Returns list of banned IPs.*/ + public Array getAdmins(){ + return admins; + } + + /**Makes a player an admin. Returns whether this player was already an admin.*/ + public boolean adminPlayer(String ip){ + if(admins.contains(ip, false)) + return false; + admins.add(ip); + saveAdmins(); + + return true; + } + + /**Makes a player no longer an admin. Returns whether this player was an admin in the first place.*/ + public boolean unAdminPlayer(String ip){ + if(!admins.contains(ip, false)) + return false; + admins.removeValue(ip, false); + saveAdmins(); + + return true; + } + + public boolean isBanned(String ip){ + return bannedIPS.contains(ip, false); + } + + public boolean isAdmin(String ip){ + return admins.contains(ip, false); + } + + private void saveKnown(){ + Settings.putString("knownIPs", json.toJson(known)); + Settings.save(); + } + + private void saveBans(){ + Settings.putString("bans", json.toJson(bannedIPS)); + Settings.save(); + } + + private void saveAdmins(){ + Settings.putString("admins", json.toJson(admins)); + Settings.save(); + } + + private void load(){ + bannedIPS = json.fromJson(Array.class, Settings.getString("bans")); + admins = json.fromJson(Array.class, Settings.getString("admins")); + known = json.fromJson(ObjectMap.class, Settings.getString("knownIPs")); + } + +} diff --git a/core/src/io/anuke/mindustry/net/Host.java b/core/src/io/anuke/mindustry/net/Host.java index 751bde7982..415b90913f 100644 --- a/core/src/io/anuke/mindustry/net/Host.java +++ b/core/src/io/anuke/mindustry/net/Host.java @@ -3,11 +3,15 @@ package io.anuke.mindustry.net; public class Host { public final String name; public final String address; + public final String mapname; + public final int wave; public final int players; - public Host(String name, String address, int players){ + public Host(String name, String address, String mapname, int wave, int players){ this.name = name; this.address = address; this.players = players; + this.mapname = mapname; + this.wave = wave; } } diff --git a/core/src/io/anuke/mindustry/net/Net.java b/core/src/io/anuke/mindustry/net/Net.java index 6473924085..5fe94ccade 100644 --- a/core/src/io/anuke/mindustry/net/Net.java +++ b/core/src/io/anuke/mindustry/net/Net.java @@ -210,7 +210,7 @@ public class Net{ } /**Pings a host in an new thread. If an error occured, failed() should be called with the exception. */ - public static void pingHost(String address, int port, Consumer valid, Consumer failed){ + public static void pingHost(String address, int port, Consumer valid, Consumer failed){ clientProvider.pingHost(address, port, valid, failed); } @@ -285,7 +285,7 @@ public class Net{ * Callback should be run on libGDX main thread.*/ void discover(Consumer> callback); /**Ping a host. If an error occured, failed() should be called with the exception. */ - void pingHost(String address, int port, Consumer valid, Consumer failed); + void pingHost(String address, int port, Consumer valid, Consumer failed); /**Close all connections.*/ void dispose(); } diff --git a/core/src/io/anuke/mindustry/net/NetEvents.java b/core/src/io/anuke/mindustry/net/NetEvents.java index 5656da784b..8b75515d9f 100644 --- a/core/src/io/anuke/mindustry/net/NetEvents.java +++ b/core/src/io/anuke/mindustry/net/NetEvents.java @@ -2,6 +2,7 @@ package io.anuke.mindustry.net; import io.anuke.mindustry.Vars; import io.anuke.mindustry.entities.BulletType; +import io.anuke.mindustry.entities.Player; import io.anuke.mindustry.entities.TileEntity; import io.anuke.mindustry.entities.enemies.Enemy; import io.anuke.mindustry.net.Net.SendMode; @@ -12,8 +13,7 @@ import io.anuke.mindustry.world.Block; import io.anuke.mindustry.world.Tile; import io.anuke.ucore.entities.Entity; -import static io.anuke.mindustry.Vars.netCommon; -import static io.anuke.mindustry.Vars.ui; +import static io.anuke.mindustry.Vars.*; public class NetEvents { @@ -99,8 +99,6 @@ public class NetEvents { packet.name = Vars.player.name; packet.id = Vars.player.id; Net.send(packet, SendMode.tcp); - - ui.chatfrag.addMessage(packet.text, netCommon.colorizeName(Vars.player.id, Vars.player.name)); } public static void handleShoot(Weapon weapon, float x, float y, float angle){ @@ -152,4 +150,27 @@ public class NetEvents { packet.itemid = (byte)item.id; Net.send(packet, SendMode.udp); } + + public static void handleAdminSet(Player player, boolean admin){ + PlayerAdminPacket packet = new PlayerAdminPacket(); + packet.admin = admin; + packet.id = player.id; + player.isAdmin = admin; + Net.send(packet, SendMode.tcp); + } + + public static void handleAdministerRequest(Player target, AdminAction action){ + AdministerRequestPacket packet = new AdministerRequestPacket(); + packet.id = target.id; + packet.action = action; + Net.send(packet, SendMode.tcp); + } + + public static void handleTraceRequest(Player target){ + if(Net.client()) { + handleAdministerRequest(target, AdminAction.trace); + }else{ + ui.traces.show(target, netServer.admins.getTrace(Net.getConnection(target.clientid).address)); + } + } } diff --git a/core/src/io/anuke/mindustry/net/NetworkIO.java b/core/src/io/anuke/mindustry/net/NetworkIO.java index 298aa78522..abfd204c0e 100644 --- a/core/src/io/anuke/mindustry/net/NetworkIO.java +++ b/core/src/io/anuke/mindustry/net/NetworkIO.java @@ -4,6 +4,7 @@ import com.badlogic.gdx.graphics.Pixmap; import com.badlogic.gdx.graphics.Pixmap.Format; import com.badlogic.gdx.utils.ByteArray; import com.badlogic.gdx.utils.TimeUtils; +import io.anuke.mindustry.entities.Player; import io.anuke.mindustry.game.GameMode; import io.anuke.mindustry.resource.Upgrade; import io.anuke.mindustry.resource.Weapon; @@ -100,7 +101,7 @@ public class NetworkIO { } } - public static void writeWorld(int playerID, ByteArray upgrades, OutputStream os){ + public static void writeWorld(Player player, ByteArray upgrades, OutputStream os){ try(DataOutputStream stream = new DataOutputStream(os)){ @@ -116,7 +117,8 @@ public class NetworkIO { stream.writeInt(state.enemies); //enemy amount stream.writeBoolean(state.friendlyFire); //friendly fire state - stream.writeInt(playerID); //player remap ID + stream.writeInt(player.id); //player remap ID + stream.writeBoolean(player.isAdmin); //--INVENTORY-- @@ -246,6 +248,7 @@ public class NetworkIO { state.friendlyFire = friendlyfire; int pid = stream.readInt(); + boolean admin = stream.readBoolean(); //inventory for(int i = 0; i < state.inventory.getItems().length; i ++){ @@ -268,6 +271,7 @@ public class NetworkIO { Entities.clear(); player.id = pid; + player.isAdmin = admin; player.add(); //map diff --git a/core/src/io/anuke/mindustry/net/Packets.java b/core/src/io/anuke/mindustry/net/Packets.java index 183fb3a8e9..a69072a6dc 100644 --- a/core/src/io/anuke/mindustry/net/Packets.java +++ b/core/src/io/anuke/mindustry/net/Packets.java @@ -8,6 +8,7 @@ import io.anuke.mindustry.io.Version; import io.anuke.mindustry.net.Packet.ImportantPacket; import io.anuke.mindustry.net.Packet.UnimportantPacket; import io.anuke.mindustry.resource.Item; +import io.anuke.mindustry.world.Block; import io.anuke.ucore.entities.Entities; import io.anuke.ucore.entities.EntityGroup; @@ -371,7 +372,7 @@ public class Packets { } public enum KickReason{ - kick, invalidPassword, clientOutdated, serverOutdated + kick, invalidPassword, clientOutdated, serverOutdated, banned } public static class UpgradePacket implements Packet{ @@ -558,4 +559,97 @@ public class Packets { itemid = buffer.get(); } } + + public static class NetErrorPacket implements Packet{ + public String message; + + @Override + public void write(ByteBuffer buffer) { + buffer.putShort((short)message.getBytes().length); + buffer.put(message.getBytes()); + } + + @Override + public void read(ByteBuffer buffer) { + short length = buffer.getShort(); + byte[] bytes = new byte[length]; + buffer.get(bytes); + message = new String(bytes); + } + } + + public static class PlayerAdminPacket implements Packet{ + public boolean admin; + public int id; + + @Override + public void write(ByteBuffer buffer) { + buffer.put(admin ? (byte)1 : 0); + buffer.putInt(id); + } + + @Override + public void read(ByteBuffer buffer) { + admin = buffer.get() == 1; + id = buffer.getInt(); + } + } + + public static class AdministerRequestPacket implements Packet{ + public AdminAction action; + public int id; + + @Override + public void write(ByteBuffer buffer) { + buffer.put((byte)action.ordinal()); + buffer.putInt(id); + } + + @Override + public void read(ByteBuffer buffer) { + action = AdminAction.values()[buffer.get()]; + id = buffer.getInt(); + } + } + + public enum AdminAction{ + kick, ban, trace + } + + public static class TracePacket implements Packet{ + public TraceInfo info; + + @Override + public void write(ByteBuffer buffer) { + buffer.putInt(info.playerid); + buffer.putShort((short)info.ip.getBytes().length); + buffer.put(info.ip.getBytes()); + buffer.put(info.modclient ? (byte)1 : 0); + + buffer.putInt(info.totalBlocksBroken); + buffer.putInt(info.structureBlocksBroken); + buffer.putInt(info.lastBlockBroken.id); + + buffer.putInt(info.totalBlocksPlaced); + buffer.putInt(info.lastBlockPlaced.id); + } + + @Override + public void read(ByteBuffer buffer) { + int id = buffer.getInt(); + short iplen = buffer.getShort(); + byte[] ipb = new byte[iplen]; + buffer.get(ipb); + + info = new TraceInfo(new String(ipb)); + + info.playerid = id; + info.modclient = buffer.get() == 1; + info.totalBlocksBroken = buffer.getInt(); + info.structureBlocksBroken = buffer.getInt(); + info.lastBlockBroken = Block.getByID(buffer.getInt()); + info.totalBlocksPlaced = buffer.getInt(); + info.lastBlockPlaced = Block.getByID(buffer.getInt()); + } + } } diff --git a/core/src/io/anuke/mindustry/net/Registrator.java b/core/src/io/anuke/mindustry/net/Registrator.java index 929cee1910..8e1dfa8297 100644 --- a/core/src/io/anuke/mindustry/net/Registrator.java +++ b/core/src/io/anuke/mindustry/net/Registrator.java @@ -40,7 +40,11 @@ public class Registrator { EntitySpawnPacket.class, ItemTransferPacket.class, ItemSetPacket.class, - ItemOffloadPacket.class + ItemOffloadPacket.class, + NetErrorPacket.class, + PlayerAdminPacket.class, + AdministerRequestPacket.class, + TracePacket.class, }; private static ObjectIntMap> ids = new ObjectIntMap<>(); diff --git a/core/src/io/anuke/mindustry/net/TraceInfo.java b/core/src/io/anuke/mindustry/net/TraceInfo.java new file mode 100644 index 0000000000..32aa036bbd --- /dev/null +++ b/core/src/io/anuke/mindustry/net/TraceInfo.java @@ -0,0 +1,21 @@ +package io.anuke.mindustry.net; + +import io.anuke.mindustry.world.Block; +import io.anuke.mindustry.world.blocks.Blocks; + +public class TraceInfo { + public int playerid; + public String ip; + public boolean modclient; + + public int totalBlocksBroken; + public int structureBlocksBroken; + public Block lastBlockBroken = Blocks.air; + + public int totalBlocksPlaced; + public Block lastBlockPlaced = Blocks.air; + + public TraceInfo(String ip){ + this.ip = ip; + } +} diff --git a/core/src/io/anuke/mindustry/ui/dialogs/AdminsDialog.java b/core/src/io/anuke/mindustry/ui/dialogs/AdminsDialog.java new file mode 100644 index 0000000000..c23348a91a --- /dev/null +++ b/core/src/io/anuke/mindustry/ui/dialogs/AdminsDialog.java @@ -0,0 +1,65 @@ +package io.anuke.mindustry.ui.dialogs; + +import io.anuke.mindustry.entities.Player; +import io.anuke.mindustry.net.Net; +import io.anuke.mindustry.net.NetConnection; +import io.anuke.mindustry.net.NetEvents; +import io.anuke.ucore.scene.ui.ScrollPane; +import io.anuke.ucore.scene.ui.layout.Table; + +import static io.anuke.mindustry.Vars.*; + +public class AdminsDialog extends FloatingDialog { + + public AdminsDialog(){ + super("$text.server.admins"); + + addCloseButton(); + + setup(); + shown(this::setup); + } + + private void setup(){ + content().clear(); + + if(gwt) return; + + float w = 400f, h = 80f; + + Table table = new Table(); + + ScrollPane pane = new ScrollPane(table, "clear"); + pane.setFadeScrollBars(false); + + if(netServer.admins.getAdmins().size == 0){ + table.add("$text.server.admins.none"); + } + + for(String ip : netServer.admins.getAdmins()){ + Table res = new Table("button"); + res.margin(14f); + + res.labelWrap("[LIGHT_GRAY]" + netServer.admins.getLastName(ip)).width(w - h - 24f); + res.add().growX(); + res.addImageButton("icon-cancel", 14*3, () -> { + ui.showConfirm("$text.confirm", "$text.confirmunadmin", () -> { + netServer.admins.unAdminPlayer(ip); + for(Player player : playerGroup.all()){ + NetConnection c = Net.getConnection(player.clientid); + if(c != null){ + NetEvents.handleAdminSet(player, false); + break; + } + } + setup(); + }); + }).size(h).pad(-14f); + + table.add(res).width(w).height(h); + table.row(); + } + + content().add(pane); + } +} diff --git a/core/src/io/anuke/mindustry/ui/dialogs/BansDialog.java b/core/src/io/anuke/mindustry/ui/dialogs/BansDialog.java new file mode 100644 index 0000000000..eea695c28a --- /dev/null +++ b/core/src/io/anuke/mindustry/ui/dialogs/BansDialog.java @@ -0,0 +1,55 @@ +package io.anuke.mindustry.ui.dialogs; + +import io.anuke.ucore.scene.ui.ScrollPane; +import io.anuke.ucore.scene.ui.layout.Table; + +import static io.anuke.mindustry.Vars.*; + +public class BansDialog extends FloatingDialog { + + public BansDialog(){ + super("$text.server.bans"); + + addCloseButton(); + + setup(); + + shown(this::setup); + } + + private void setup(){ + content().clear(); + + if(gwt) return; + + float w = 400f, h = 80f; + + Table table = new Table(); + + ScrollPane pane = new ScrollPane(table, "clear"); + pane.setFadeScrollBars(false); + + if(netServer.admins.getBanned().size == 0){ + table.add("$text.server.bans.none"); + } + + for(String ip : netServer.admins.getBanned()){ + Table res = new Table("button"); + res.margin(14f); + + res.labelWrap("IP: [LIGHT_GRAY]" + ip + "\n[]Name: [LIGHT_GRAY]" + netServer.admins.getLastName(ip)).width(w - h - 24f); + res.add().growX(); + res.addImageButton("icon-cancel", 14*3, () -> { + ui.showConfirm("$text.confirm", "$text.confirmunban", () -> { + netServer.admins.unbanPlayer(ip); + setup(); + }); + }).size(h).pad(-14f); + + table.add(res).width(w).height(h); + table.row(); + } + + content().add(pane); + } +} diff --git a/core/src/io/anuke/mindustry/ui/dialogs/HostDialog.java b/core/src/io/anuke/mindustry/ui/dialogs/HostDialog.java index d4991027bf..51ee677775 100644 --- a/core/src/io/anuke/mindustry/ui/dialogs/HostDialog.java +++ b/core/src/io/anuke/mindustry/ui/dialogs/HostDialog.java @@ -14,7 +14,6 @@ import java.io.IOException; import static io.anuke.mindustry.Vars.player; import static io.anuke.mindustry.Vars.ui; -//TODO add port specification public class HostDialog extends FloatingDialog{ float w = 300; @@ -31,13 +30,13 @@ public class HostDialog extends FloatingDialog{ Settings.put("name", text); Settings.save(); ui.listfrag.rebuild(); - }).grow().pad(8); + }).grow().pad(8).get().setMaxLength(40); ImageButton button = t.addImageButton("white", 40, () -> { new ColorPickDialog().show(color -> { player.color.set(color); Settings.putInt("color", Color.rgba8888(color)); - Settings.save();; + Settings.save(); }); }).size(50f, 54f).get(); button.update(() -> button.getStyle().imageUpColor = player.getColor()); @@ -50,6 +49,7 @@ public class HostDialog extends FloatingDialog{ Timers.runTask(5f, () -> { try{ Net.host(Vars.port); + player.isAdmin = true; }catch (IOException e){ ui.showError(Bundles.format("text.server.error", Strings.parseException(e, false))); } @@ -58,19 +58,4 @@ public class HostDialog extends FloatingDialog{ }); }).width(w).height(70f); } - - /* - showTextInput("$text.hostserver", "$text.server.port", Vars.port + "", new DigitsOnlyFilter(), text -> { - int result = Strings.parseInt(text); - if(result == Integer.MIN_VALUE || result >= 65535){ - ui.showError("$text.server.invalidport"); - }else{ - try{ - Net.host(result); - }catch (IOException e){ - ui.showError(Bundles.format("text.server.error", Strings.parseException(e, false))); - } - } - }); - */ } diff --git a/core/src/io/anuke/mindustry/ui/dialogs/JoinDialog.java b/core/src/io/anuke/mindustry/ui/dialogs/JoinDialog.java index 8a5234ee0d..e62efde4d9 100644 --- a/core/src/io/anuke/mindustry/ui/dialogs/JoinDialog.java +++ b/core/src/io/anuke/mindustry/ui/dialogs/JoinDialog.java @@ -56,7 +56,6 @@ public class JoinDialog extends FloatingDialog { setupRemote(); refreshRemote(); }else{ - //renaming.port = Strings.parseInt(Settings.getString("port")); renaming.ip = Settings.getString("ip"); saveServers(); setupRemote(); @@ -85,7 +84,7 @@ public class JoinDialog extends FloatingDialog { TextButton button = buttons[0] = remote.addButton("[accent]"+server.ip, "clear", () -> { if(!buttons[0].childrenPressed()) connect(server.ip, Vars.port); - }).width(w).height(120f).pad(4f).get(); + }).width(w).height(140f).pad(4f).get(); button.getLabel().setWrap(true); @@ -134,10 +133,14 @@ public class JoinDialog extends FloatingDialog { Net.pingHost(server.ip, server.port, host -> { server.content.clear(); - server.content.add("[lightgray]" + Bundles.format("text.server.hostname", host.name)).pad(4); + server.content.add("[lightgray]" + Bundles.format("text.server.hostname", host.name)).left(); server.content.row(); server.content.add("[lightgray]" + (host.players != 1 ? Bundles.format("text.players", host.players) : - Bundles.format("text.players.single", host.players))); + Bundles.format("text.players.single", host.players))).left(); + server.content.row(); + server.content.add("[lightgray]" + Bundles.format("text.save.map", host.mapname)).left(); + server.content.row(); + server.content.add("[lightgray]" + Bundles.format("text.save.wave", host.wave)).left(); }, e -> { server.content.clear(); server.content.add("$text.host.invalid"); @@ -175,7 +178,7 @@ public class JoinDialog extends FloatingDialog { Vars.player.name = text; Settings.put("name", text); Settings.save(); - }).grow().pad(8); + }).grow().pad(8).get().setMaxLength(40); ImageButton button = t.addImageButton("white", 40, () -> { new ColorPickDialog().show(color -> { diff --git a/core/src/io/anuke/mindustry/ui/dialogs/SettingsMenuDialog.java b/core/src/io/anuke/mindustry/ui/dialogs/SettingsMenuDialog.java index ca0c126cf6..6d2e79c610 100644 --- a/core/src/io/anuke/mindustry/ui/dialogs/SettingsMenuDialog.java +++ b/core/src/io/anuke/mindustry/ui/dialogs/SettingsMenuDialog.java @@ -32,18 +32,18 @@ public class SettingsMenuDialog extends SettingsDialog{ public SettingsMenuDialog(){ setStyle(Core.skin.get("dialog", WindowStyle.class)); - hidden(()->{ + hidden(() -> { if(!state.is(State.menu)){ if(!wasPaused || Net.active()) state.set(State.playing); } }); - shown(()->{ + shown(() -> { if(!state.is(State.menu)){ wasPaused = state.is(State.paused); - if(menu.getScene() != null){ - wasPaused = ((PausedDialog)menu).wasPaused; + if(ui.paused.getScene() != null){ + wasPaused = ui.paused.wasPaused; } if(!Net.active()) state.set(State.paused); ui.paused.hide(); diff --git a/core/src/io/anuke/mindustry/ui/dialogs/TraceDialog.java b/core/src/io/anuke/mindustry/ui/dialogs/TraceDialog.java new file mode 100644 index 0000000000..be1bf4f5b6 --- /dev/null +++ b/core/src/io/anuke/mindustry/ui/dialogs/TraceDialog.java @@ -0,0 +1,53 @@ +package io.anuke.mindustry.ui.dialogs; + +import io.anuke.mindustry.entities.Player; +import io.anuke.mindustry.net.TraceInfo; +import io.anuke.ucore.scene.ui.layout.Table; +import io.anuke.ucore.util.Bundles; + +public class TraceDialog extends FloatingDialog { + + public TraceDialog(){ + super("$text.trace"); + + addCloseButton(); + } + + public void show(Player player, TraceInfo info){ + content().clear(); + + Table table = new Table("button"); + table.margin(14); + table.defaults().pad(1); + + table.defaults().left(); + table.add(Bundles.format("text.trace.playername", player.name)); + table.row(); + table.add(Bundles.format("text.trace.ip", info.ip)); + table.row(); + table.add(Bundles.format("text.trace.modclient", info.modclient)); + table.row(); + + table.add().pad(5); + table.row(); + + table.add(Bundles.format("text.trace.totalblocksbroken", info.totalBlocksBroken)); + table.row(); + table.add(Bundles.format("text.trace.structureblocksbroken", info.structureBlocksBroken)); + table.row(); + table.add(Bundles.format("text.trace.lastblockbroken", info.lastBlockBroken.formalName)); + table.row(); + + table.add().pad(5); + table.row(); + + table.add(Bundles.format("text.trace.totalblocksplaced", info.totalBlocksPlaced)); + table.row(); + table.add(Bundles.format("text.trace.lastblockplaced", info.lastBlockPlaced.formalName)); + table.row(); + + content().add(table); + + show(); + } +} diff --git a/core/src/io/anuke/mindustry/ui/fragments/BackgroundFragment.java b/core/src/io/anuke/mindustry/ui/fragments/BackgroundFragment.java index f660fe6616..8cd8abbb6e 100644 --- a/core/src/io/anuke/mindustry/ui/fragments/BackgroundFragment.java +++ b/core/src/io/anuke/mindustry/ui/fragments/BackgroundFragment.java @@ -24,13 +24,14 @@ public class BackgroundFragment implements Fragment { Core.batch.draw(back, w/2 - back.getRegionWidth()*backscl/2 +240f, h/2 - back.getRegionHeight()*backscl/2 + 250f, back.getRegionWidth()*backscl, back.getRegionHeight()*backscl); - float logoscl = (int)Unit.dp.scl(7); + boolean portrait = Gdx.graphics.getWidth() < Gdx.graphics.getHeight(); + float logoscl = (int)Unit.dp.scl(7) * (portrait ? 5f/7f : 1f); TextureRegion logo = Core.skin.getRegion("logotext"); float logow = logo.getRegionWidth()*logoscl; float logoh = logo.getRegionHeight()*logoscl; Draw.color(); - Core.batch.draw(logo, w/2 - logow/2, h - logoh + 15, logow, logoh); + Core.batch.draw(logo, w/2 - logow/2, h - logoh + 15 + (portrait ? -Unit.dp.scl(30f) : 0f), logow, logoh); }).visible(() -> state.is(State.menu)).grow(); } } diff --git a/core/src/io/anuke/mindustry/ui/fragments/DebugFragment.java b/core/src/io/anuke/mindustry/ui/fragments/DebugFragment.java index a8ec97aaaf..bc71c1312f 100644 --- a/core/src/io/anuke/mindustry/ui/fragments/DebugFragment.java +++ b/core/src/io/anuke/mindustry/ui/fragments/DebugFragment.java @@ -57,11 +57,22 @@ public class DebugFragment implements Fragment { row(); new button("wave", () -> state.wavetime = 0f); row(); + new button("time 0", () -> Timers.resetTime(0f)); + row(); + new button("time max", () -> Timers.resetTime(1080000 - 60*10)); + row(); + new button("clear", () -> { + enemyGroup.clear(); + state.enemies = 0; + netClient.clearRecieved(); + }); + row(); new button("spawn", () -> { for(int i = 0; i < 30; i ++){ new Enemy(EnemyTypes.healer).set(player.x + Mathf.range(50f), player.y + Mathf.range(50f)).add(); } }); + row(); }}.end(); row(); diff --git a/core/src/io/anuke/mindustry/ui/fragments/MenuFragment.java b/core/src/io/anuke/mindustry/ui/fragments/MenuFragment.java index 9dd218414c..c972b744d6 100644 --- a/core/src/io/anuke/mindustry/ui/fragments/MenuFragment.java +++ b/core/src/io/anuke/mindustry/ui/fragments/MenuFragment.java @@ -16,10 +16,10 @@ import static io.anuke.mindustry.Vars.*; public class MenuFragment implements Fragment{ public void build(){ - if(!android){ - //menu table - new table(){{ - + new table(){{ + visible(() -> state.is(State.menu)); + + if(!android){ new table(){{ PressGroup group = new PressGroup(); @@ -53,37 +53,32 @@ public class MenuFragment implements Fragment{ } get().margin(16); }}.end(); - - visible(() -> state.is(State.menu)); - }}.end(); - }else{ - new table(){{ - new table(){{ + + }else { + new table() {{ defaults().size(120f).pad(5); - float isize = 14f*4; - + float isize = 14f * 4; + new imagebutton("icon-play-2", isize, ui.levels::show).text("$text.play").padTop(4f); - + new imagebutton("icon-tutorial", isize, () -> control.playMap(world.maps().getMap("tutorial"))).text("$text.tutorial").padTop(4f); - + new imagebutton("icon-load", isize, ui.load::show).text("$text.load").padTop(4f); new imagebutton("icon-add", isize, ui.join::show).text("$text.joingame").padTop(4f); - + row(); new imagebutton("icon-editor", isize, ui.editor::show).text("$text.editor").padTop(4f); - + new imagebutton("icon-tools", isize, ui.settings::show).text("$text.settings").padTop(4f); new imagebutton("icon-info", isize, ui.about::show).text("$text.about.button").padTop(4f); new imagebutton("icon-donate", isize, Platform.instance::openDonations).text("$text.donate").padTop(4f); - - visible(() -> state.is(State.menu)); }}.end(); - }}.end(); - } + } + }}.end(); //extra icons in top right new table(){{ diff --git a/core/src/io/anuke/mindustry/ui/fragments/PlacementFragment.java b/core/src/io/anuke/mindustry/ui/fragments/PlacementFragment.java index 046b056e80..39427f4cf3 100644 --- a/core/src/io/anuke/mindustry/ui/fragments/PlacementFragment.java +++ b/core/src/io/anuke/mindustry/ui/fragments/PlacementFragment.java @@ -214,6 +214,7 @@ public class PlacementFragment implements Fragment{ breaktable.getParent().swapActor(breaktable, next); if(!show){ + control.input().breakMode = PlaceMode.none; breaktable.actions(Actions.translateBy(-breaktable.getWidth() - 5, 0, dur, in), Actions.call(() -> shown = false)); }else{ shown = true; diff --git a/core/src/io/anuke/mindustry/ui/fragments/PlayerListFragment.java b/core/src/io/anuke/mindustry/ui/fragments/PlayerListFragment.java index 0933cee7b7..19cb02e7c7 100644 --- a/core/src/io/anuke/mindustry/ui/fragments/PlayerListFragment.java +++ b/core/src/io/anuke/mindustry/ui/fragments/PlayerListFragment.java @@ -1,16 +1,21 @@ package io.anuke.mindustry.ui.fragments; +import io.anuke.mindustry.Vars; import io.anuke.mindustry.core.GameState.State; import io.anuke.mindustry.entities.Player; import io.anuke.mindustry.net.Net; +import io.anuke.mindustry.net.NetConnection; import io.anuke.mindustry.net.NetEvents; +import io.anuke.mindustry.net.Packets.AdminAction; import io.anuke.mindustry.net.Packets.KickReason; import io.anuke.mindustry.ui.BorderImage; import io.anuke.ucore.core.Inputs; import io.anuke.ucore.graphics.Draw; import io.anuke.ucore.scene.Element; +import io.anuke.ucore.scene.builders.button; import io.anuke.ucore.scene.builders.label; import io.anuke.ucore.scene.builders.table; +import io.anuke.ucore.scene.event.Touchable; import io.anuke.ucore.scene.ui.ScrollPane; import io.anuke.ucore.scene.ui.layout.Stack; import io.anuke.ucore.scene.ui.layout.Table; @@ -40,10 +45,20 @@ public class PlayerListFragment implements Fragment{ row(); new table("pane"){{ margin(12f); + get().addCheck("$text.server.friendlyfire", b -> { state.friendlyFire = b; NetEvents.handleFriendlyFireChange(b); - }).growX().update(i -> i.setChecked(state.friendlyFire)).disabled(b -> Net.client()); + }).growX().update(i -> i.setChecked(state.friendlyFire)).disabled(b -> Net.client()).padRight(5); + + new button("$text.server.bans", () -> { + ui.bans.show(); + }).padTop(-12).padBottom(-12).fillY().cell.disabled(b -> Net.client()); + + new button("$text.server.admins", () -> { + ui.admins.show(); + }).padTop(-12).padBottom(-12).padRight(-12).fillY().cell.disabled(b -> Net.client()); + }}.pad(10f).growX().end(); }}.end(); @@ -69,9 +84,13 @@ public class PlayerListFragment implements Fragment{ public void rebuild(){ content.clear(); - float h = 60f; + float h = 74f; for(Player player : playerGroup.all()){ + NetConnection connection = gwt ? null : Net.getConnection(player.clientid); + + if(connection == null && Net.server() && !player.isLocal) continue; + Table button = new Table("button"); button.left(); button.margin(5).marginBottom(10); @@ -93,19 +112,68 @@ public class PlayerListFragment implements Fragment{ } }); } - button.add(stack).size(h); - button.add("[#" + player.getColor().toString().toUpperCase() + "]" + player.name).pad(10); + button.labelWrap("[#" + player.getColor().toString().toUpperCase() + "]" + player.name).width(170f).pad(10); button.add().grow(); - if(Net.server() && !player.isLocal){ + button.addImage("icon-admin").size(14*2).visible(() -> player.isAdmin && !(!player.isLocal && Net.server())).padRight(5); + + if((Net.server() || Vars.player.isAdmin) && !player.isLocal && (!player.isAdmin || Net.server())){ button.add().growY(); - button.addImageButton("icon-cancel", 14*3, () -> - Net.kickConnection(player.clientid, KickReason.kick) - ).pad(-5).padBottom(-10).size(h+10, h+14); + + float bs = (h + 14)/2f; + + button.table(t -> { + t.defaults().size(bs - 1, bs + 3); + + t.addImageButton("icon-ban", 14*2, () -> { + ui.showConfirm("$text.confirm", "$text.confirmban", () -> { + if(Net.server()) { + netServer.admins.banPlayer(connection.address); + Net.kickConnection(player.clientid, KickReason.banned); + }else{ + NetEvents.handleAdministerRequest(player, AdminAction.ban); + } + }); + }).padBottom(-5.1f); + + t.addImageButton("icon-cancel", 14*2, () -> { + if(Net.server()) { + Net.kickConnection(player.clientid, KickReason.kick); + }else{ + NetEvents.handleAdministerRequest(player, AdminAction.kick); + } + }).padBottom(-5.1f); + + t.row(); + + t.addImageButton("icon-admin", "toggle", 14*2, () -> { + if(Net.client()) return; + + if(netServer.admins.isAdmin(connection.address)){ + ui.showConfirm("$text.confirm", "$text.confirmunadmin", () -> { + netServer.admins.unAdminPlayer(connection.address); + NetEvents.handleAdminSet(player, false); + }); + }else{ + ui.showConfirm("$text.confirm", "$text.confirmadmin", () -> { + netServer.admins.adminPlayer(connection.address); + NetEvents.handleAdminSet(player, true); + }); + } + }).update(b ->{ + b.setChecked(player.isAdmin); + b.setDisabled(Net.client()); + }).get().setTouchable(() -> Net.client() ? Touchable.disabled : Touchable.enabled); + + t.addImageButton("icon-zoom-small", 14*2, () -> NetEvents.handleTraceRequest(player)); + + }).padRight(12).padTop(-5).padLeft(0).padBottom(-10).size(bs + 10f, bs); + + } - content.add(button).padBottom(-5).width(350f); + content.add(button).padBottom(-6).width(350f).maxHeight(h + 14); content.row(); } diff --git a/core/src/io/anuke/mindustry/world/Placement.java b/core/src/io/anuke/mindustry/world/Placement.java new file mode 100644 index 0000000000..f653cba590 --- /dev/null +++ b/core/src/io/anuke/mindustry/world/Placement.java @@ -0,0 +1,166 @@ +package io.anuke.mindustry.world; + +import com.badlogic.gdx.math.Rectangle; +import com.badlogic.gdx.math.Vector2; +import com.badlogic.gdx.utils.Array; +import io.anuke.mindustry.entities.Player; +import io.anuke.mindustry.game.SpawnPoint; +import io.anuke.mindustry.graphics.Fx; +import io.anuke.mindustry.resource.ItemStack; +import io.anuke.mindustry.resource.Recipe; +import io.anuke.mindustry.resource.Recipes; +import io.anuke.mindustry.world.blocks.Blocks; +import io.anuke.mindustry.world.blocks.ProductionBlocks; +import io.anuke.ucore.core.Effects; +import io.anuke.ucore.core.Sounds; +import io.anuke.ucore.entities.Entities; +import io.anuke.ucore.entities.SolidEntity; + +import static io.anuke.mindustry.Vars.*; + +public class Placement { + private static final Rectangle rect = new Rectangle(); + + /**Returns block type that was broken, or null if unsuccesful.*/ + public static Block breakBlock(int x, int y, boolean effect, boolean sound){ + Tile tile = world.tile(x, y); + + if(tile == null) return null; + + Block block = tile.isLinked() ? tile.getLinked().block() : tile.block(); + Recipe result = Recipes.getByResult(block); + + if(result != null){ + for(ItemStack stack : result.requirements){ + state.inventory.addItem(stack.item, (int)(stack.amount * breakDropAmount)); + } + } + + if(tile.block().drops != null){ + state.inventory.addItem(tile.block().drops.item, tile.block().drops.amount); + } + + if(sound) Sounds.play("break"); + + if(!tile.block().isMultiblock() && !tile.isLinked()){ + tile.setBlock(Blocks.air); + if(effect) Effects.effect(Fx.breakBlock, tile.worldx(), tile.worldy()); + }else{ + Tile target = tile.isLinked() ? tile.getLinked() : tile; + Array removals = target.getLinkedTiles(); + for(Tile toremove : removals){ + //note that setting a new block automatically unlinks it + toremove.setBlock(Blocks.air); + if(effect) Effects.effect(Fx.breakBlock, toremove.worldx(), toremove.worldy()); + } + } + + return block; + } + + public static void placeBlock(int x, int y, Block result, int rotation, boolean effects, boolean sound){ + Tile tile = world.tile(x, y); + + //just in case + if(tile == null) return; + + tile.setBlock(result, rotation); + + if(result.isMultiblock()){ + int offsetx = -(result.width-1)/2; + int offsety = -(result.height-1)/2; + + for(int dx = 0; dx < result.width; dx ++){ + for(int dy = 0; dy < result.height; dy ++){ + int worldx = dx + offsetx + x; + int worldy = dy + offsety + y; + if(!(worldx == x && worldy == y)){ + Tile toplace = world.tile(worldx, worldy); + if(toplace != null) + toplace.setLinked((byte)(dx + offsetx), (byte)(dy + offsety)); + } + + if(effects) Effects.effect(Fx.place, worldx * tilesize, worldy * tilesize); + } + } + }else if(effects) Effects.effect(Fx.place, x * tilesize, y * tilesize); + + if(effects && sound) Sounds.play("place"); + } + + public static boolean validPlace(int x, int y, Block type){ + for(int i = 0; i < world.getSpawns().size; i ++){ + SpawnPoint spawn = world.getSpawns().get(i); + if(Vector2.dst(x * tilesize, y * tilesize, spawn.start.worldx(), spawn.start.worldy()) < enemyspawnspace){ + return false; + } + } + + Recipe recipe = Recipes.getByResult(type); + + if(recipe == null || !state.inventory.hasItems(recipe.requirements)){ + return false; + } + + rect.setSize(type.width * tilesize, type.height * tilesize); + Vector2 offset = type.getPlaceOffset(); + rect.setCenter(offset.x + x * tilesize, offset.y + y * tilesize); + + synchronized (Entities.entityLock) { + for (SolidEntity e : Entities.getNearby(enemyGroup, x * tilesize, y * tilesize, tilesize * 2f)) { + if (e == null) continue; //not sure why this happens? + Rectangle rect = e.hitbox.getRect(e.x, e.y); + + if (Placement.rect.overlaps(rect)) { + return false; + } + } + } + + if(type.solid || type.solidifes) { + for (Player player : playerGroup.all()) { + if (!player.isAndroid && rect.overlaps(player.hitbox.getRect(player.x, player.y))) { + return false; + } + } + } + + Tile tile = world.tile(x, y); + + if(tile == null || (isSpawnPoint(tile) && (type.solidifes || type.solid))) return false; + + if(type.isMultiblock()){ + int offsetx = -(type.width-1)/2; + int offsety = -(type.height-1)/2; + for(int dx = 0; dx < type.width; dx ++){ + for(int dy = 0; dy < type.height; dy ++){ + Tile other = world.tile(x + dx + offsetx, y + dy + offsety); + if(other == null || (other.block() != Blocks.air && !other.block().alwaysReplace) || isSpawnPoint(other)){ + return false; + } + } + } + return true; + }else { + return tile.block() != type + && (type.canReplace(tile.block()) || tile.block().alwaysReplace) + && tile.block().isMultiblock() == type.isMultiblock() || tile.block() == Blocks.air; + } + } + + public static boolean isSpawnPoint(Tile tile){ + return tile != null && tile.x == world.getCore().x && tile.y == world.getCore().y - 2; + } + + public static boolean validBreak(int x, int y){ + Tile tile = world.tile(x, y); + + if(tile == null || tile.block() == ProductionBlocks.core) return false; + + if(tile.isLinked() && tile.getLinked().block() == ProductionBlocks.core){ + return false; + } + + return tile.breakable(); + } +} diff --git a/core/src/io/anuke/mindustry/world/blocks/types/defense/LaserTurret.java b/core/src/io/anuke/mindustry/world/blocks/types/defense/LaserTurret.java index 16efe1fcd2..fb75fbd9b2 100644 --- a/core/src/io/anuke/mindustry/world/blocks/types/defense/LaserTurret.java +++ b/core/src/io/anuke/mindustry/world/blocks/types/defense/LaserTurret.java @@ -43,13 +43,14 @@ public class LaserTurret extends PowerTurret{ @Override public void drawLayer2(Tile tile){ TurretEntity entity = tile.entity(); + Enemy enemy = entity.target; - if(entity.target != null && - Angles.angleDist(entity.rotation, Angles.angle(tile.drawx(), tile.drawy(), entity.target.x, entity.target.y)) <= cone){ + if(enemy != null && + Angles.angleDist(entity.rotation, Angles.angle(tile.drawx(), tile.drawy(), enemy.x, enemy.y)) <= cone){ float len = 4f; float x = tile.drawx() + Angles.trnsx(entity.rotation, len), y = tile.drawy() + Angles.trnsy(entity.rotation, len); - float x2 = entity.target.x, y2 = entity.target.y; + float x2 = enemy.x, y2 = enemy.y; float lighten = (MathUtils.sin(Timers.time()/1.2f) + 1f) / 10f; diff --git a/core/src/io/anuke/mindustry/world/blocks/types/defense/RepairTurret.java b/core/src/io/anuke/mindustry/world/blocks/types/defense/RepairTurret.java index 4e65b5dc6e..1d650e08fc 100644 --- a/core/src/io/anuke/mindustry/world/blocks/types/defense/RepairTurret.java +++ b/core/src/io/anuke/mindustry/world/blocks/types/defense/RepairTurret.java @@ -3,6 +3,7 @@ package io.anuke.mindustry.world.blocks.types.defense; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.math.MathUtils; import com.badlogic.gdx.utils.Array; +import io.anuke.mindustry.entities.TileEntity; import io.anuke.mindustry.world.Layer; import io.anuke.mindustry.world.Tile; import io.anuke.ucore.core.Timers; @@ -80,9 +81,10 @@ public class RepairTurret extends PowerTurret{ @Override public void drawLayer2(Tile tile){ PowerTurretEntity entity = tile.entity(); + TileEntity target = entity.blockTarget; - if(entity.power >= powerUsed && entity.blockTarget != null && Angles.angleDist(entity.angleTo(entity.blockTarget), entity.rotation) < 10){ - Tile targetTile = entity.blockTarget.tile; + if(entity.power >= powerUsed && target != null && Angles.angleDist(entity.angleTo(target), entity.rotation) < 10){ + Tile targetTile = target.tile; float len = 4f; float x = tile.drawx() + Angles.trnsx(entity.rotation, len), y = tile.drawy() + Angles.trnsy(entity.rotation, len); diff --git a/core/src/io/anuke/mindustry/world/blocks/types/distribution/Junction.java b/core/src/io/anuke/mindustry/world/blocks/types/distribution/Junction.java index 177203884d..bfaa422cd0 100644 --- a/core/src/io/anuke/mindustry/world/blocks/types/distribution/Junction.java +++ b/core/src/io/anuke/mindustry/world/blocks/types/distribution/Junction.java @@ -84,6 +84,7 @@ public class Junction extends Block{ return new JunctionEntity(); } + @Override public Array getDebugInfo(Tile tile){ JunctionEntity entity = tile.entity(); Array arr = super.getDebugInfo(tile); diff --git a/core/src/io/anuke/mindustry/world/blocks/types/distribution/TunnelConveyor.java b/core/src/io/anuke/mindustry/world/blocks/types/distribution/TunnelConveyor.java index 16e51b0556..0e6d91c202 100644 --- a/core/src/io/anuke/mindustry/world/blocks/types/distribution/TunnelConveyor.java +++ b/core/src/io/anuke/mindustry/world/blocks/types/distribution/TunnelConveyor.java @@ -1,5 +1,6 @@ package io.anuke.mindustry.world.blocks.types.distribution; +import com.badlogic.gdx.utils.Array; import com.badlogic.gdx.utils.NumberUtils; import io.anuke.mindustry.entities.TileEntity; import io.anuke.mindustry.resource.Item; @@ -90,10 +91,35 @@ public class TunnelConveyor extends Block{ return new TunnelEntity(); } + @Override + public Array getDebugInfo(Tile tile){ + TunnelEntity entity = tile.entity(); + Array arr = super.getDebugInfo(tile); + for(int i = 0; i < 4; i ++){ + arr.add("nearby." + i); + arr.add(tile.getNearby(i)); + } + + arr.add("buffer"); + arr.add(entity.index); + + for(int i = 0; i < entity.index; i++){ + long l = entity.items[i]; + float time = NumberUtils.intBitsToFloat(Bits.getLeftInt(l)); + Item item = Item.getByID(Bits.getRightInt(l)); + Tile dest = getDestTunnel(tile, item); + arr.add(" buffer.item"); + arr.add(time + " | " + item.name + " | " + ( dest == null ? "no dest" : dest.block() + ":" + dest.floor())); + } + + return arr; + } + Tile getDestTunnel(Tile tile, Item item){ Tile dest = tile; int rel = (tile.getRotation() + 2)%4; for(int i = 0; i < maxdist; i ++){ + if(dest == null) return null; dest = dest.getNearby(rel); if(dest != null && dest.block() instanceof TunnelConveyor && dest.getRotation() == rel && dest.getNearby(rel) != null diff --git a/html/src/io/anuke/mindustry/client/WebsocketClient.java b/html/src/io/anuke/mindustry/client/WebsocketClient.java index 7118e45c5f..66b82fe32a 100644 --- a/html/src/io/anuke/mindustry/client/WebsocketClient.java +++ b/html/src/io/anuke/mindustry/client/WebsocketClient.java @@ -103,7 +103,7 @@ public class WebsocketClient implements ClientProvider { } @Override - public void pingHost(String address, int port, Consumer valid, Consumer failed) { + public void pingHost(String address, int port, Consumer valid, Consumer failed) { if(!Platform.instance.canJoinGame()) { failed.accept(new IOException()); }else { @@ -119,7 +119,7 @@ public class WebsocketClient implements ClientProvider { public void onMessage(String msg) { if(!msg.startsWith("---")) return; String[] text = msg.substring(3).split("\\|"); - Host host = new Host(text[1], address, Strings.parseInt(text[0])); + Host host = new Host(text[1], address, text[2], Strings.parseInt(text[3]), Strings.parseInt(text[0])); valid.accept(host); accepted[0] = true; socket.close(); diff --git a/kryonet/src/io/anuke/kryonet/KryoClient.java b/kryonet/src/io/anuke/kryonet/KryoClient.java index 0940f4e517..77cf87be44 100644 --- a/kryonet/src/io/anuke/kryonet/KryoClient.java +++ b/kryonet/src/io/anuke/kryonet/KryoClient.java @@ -13,6 +13,7 @@ import io.anuke.mindustry.net.Net.SendMode; import io.anuke.mindustry.net.Packets.Connect; import io.anuke.mindustry.net.Packets.Disconnect; import io.anuke.ucore.function.Consumer; +import io.anuke.ucore.util.Strings; import java.io.IOException; import java.net.DatagramPacket; @@ -34,7 +35,7 @@ public class KryoClient implements ClientProvider{ handler = new ClientDiscoveryHandler() { @Override public DatagramPacket onRequestNewDatagramPacket() { - return new DatagramPacket(new byte[32], 32); + return new DatagramPacket(new byte[128], 128); } @Override @@ -140,7 +141,7 @@ public class KryoClient implements ClientProvider{ } @Override - public void pingHost(String address, int port, Consumer valid, Consumer invalid){ + public void pingHost(String address, int port, Consumer valid, Consumer invalid){ runAsync(() -> { try { DatagramSocket socket = new DatagramSocket(); @@ -162,7 +163,7 @@ public class KryoClient implements ClientProvider{ } else { Gdx.app.postRunnable(() -> invalid.accept(new IOException("Outdated server."))); } - } catch (IOException e) { + } catch (Exception e) { Gdx.app.postRunnable(() -> invalid.accept(e)); } }); @@ -209,7 +210,7 @@ public class KryoClient implements ClientProvider{ if(e instanceof KryoNetException){ Gdx.app.postRunnable(() -> Net.showError("$text.server.mismatch")); }else{ - //TODO better exception handling. + Net.showError(Strings.parseException(e, true)); disconnect(); } } diff --git a/kryonet/src/io/anuke/kryonet/KryoRegistrator.java b/kryonet/src/io/anuke/kryonet/KryoRegistrator.java index a30486cbce..007580013b 100644 --- a/kryonet/src/io/anuke/kryonet/KryoRegistrator.java +++ b/kryonet/src/io/anuke/kryonet/KryoRegistrator.java @@ -2,7 +2,6 @@ package io.anuke.kryonet; import com.esotericsoftware.minlog.Log; import com.esotericsoftware.minlog.Log.Logger; -import io.anuke.mindustry.Vars; import io.anuke.mindustry.net.Host; import io.anuke.ucore.util.ColorCodes; @@ -11,8 +10,7 @@ import java.io.StringWriter; import java.net.InetAddress; import java.nio.ByteBuffer; -import static io.anuke.mindustry.Vars.headless; -import static io.anuke.mindustry.Vars.playerGroup; +import static io.anuke.mindustry.Vars.*; public class KryoRegistrator { public static boolean fakeLag = false; @@ -49,24 +47,44 @@ public class KryoRegistrator { } public static ByteBuffer writeServerData(){ - String host = headless ? "Server" : Vars.player.name; + int maxlen = 32; + + String host = (headless ? "Server" : player.name); + String map = world.getMap().name; + + host = host.substring(0, Math.min(host.length(), maxlen)); + map = map.substring(0, Math.min(map.length(), maxlen)); + + ByteBuffer buffer = ByteBuffer.allocate(128); - ByteBuffer buffer = ByteBuffer.allocate(1 + host.getBytes().length + 4); buffer.put((byte)host.getBytes().length); buffer.put(host.getBytes()); + + buffer.put((byte)map.getBytes().length); + buffer.put(map.getBytes()); + buffer.putInt(playerGroup.size()); + buffer.putInt(state.wave); return buffer; } public static Host readServerData(InetAddress ia, ByteBuffer buffer){ - //old version address. - if(buffer.capacity() == 4) return null; + if(buffer.capacity() < 128) return null; //old version address. + + byte hlength = buffer.get(); + byte[] hb = new byte[hlength]; + buffer.get(hb); + + byte mlength = buffer.get(); + byte[] mb = new byte[mlength]; + buffer.get(mb); + + String host = new String(hb); + String map = new String(mb); - byte length = buffer.get(); - byte[] sname = new byte[length]; - buffer.get(sname); int players = buffer.getInt(); + int wave = buffer.getInt(); - return new Host(new String(sname), ia.getHostAddress(), players); + return new Host(host, ia.getHostAddress(), map, wave, players); } } diff --git a/kryonet/src/io/anuke/kryonet/KryoServer.java b/kryonet/src/io/anuke/kryonet/KryoServer.java index 7e8cacfeb4..62332f841f 100644 --- a/kryonet/src/io/anuke/kryonet/KryoServer.java +++ b/kryonet/src/io/anuke/kryonet/KryoServer.java @@ -14,10 +14,7 @@ import io.anuke.mindustry.net.Net; import io.anuke.mindustry.net.Net.SendMode; import io.anuke.mindustry.net.Net.ServerProvider; import io.anuke.mindustry.net.NetConnection; -import io.anuke.mindustry.net.Packets.Connect; -import io.anuke.mindustry.net.Packets.Disconnect; -import io.anuke.mindustry.net.Packets.KickPacket; -import io.anuke.mindustry.net.Packets.KickReason; +import io.anuke.mindustry.net.Packets.*; import io.anuke.mindustry.net.Registrator; import io.anuke.mindustry.net.Streamable; import io.anuke.mindustry.net.Streamable.StreamBegin; @@ -25,6 +22,7 @@ import io.anuke.mindustry.net.Streamable.StreamChunk; import io.anuke.ucore.UCore; import io.anuke.ucore.core.Timers; import io.anuke.ucore.util.Log; +import io.anuke.ucore.util.Strings; import org.java_websocket.WebSocket; import org.java_websocket.exceptions.WebsocketNotConnectedException; import org.java_websocket.handshake.ClientHandshake; @@ -66,13 +64,15 @@ public class KryoServer implements ServerProvider { @Override public void connected (Connection connection) { - KryoConnection kn = new KryoConnection(lastconnection ++, connection.getRemoteAddressTCP().toString(), connection); + String ip = connection.getRemoteAddressTCP().getAddress().getHostAddress(); + + KryoConnection kn = new KryoConnection(lastconnection ++, ip, connection); Connect c = new Connect(); c.id = kn.id; - c.addressTCP = connection.getRemoteAddressTCP().toString(); + c.addressTCP = ip; - Log.info("&bRecieved connection: {0} {1}", c.id, c.addressTCP); + Log.info("&bRecieved connection: {0} / {1}", c.id, c.addressTCP); connections.add(kn); Gdx.app.postRunnable(() -> Net.handleServerReceived(kn.id, c)); @@ -141,12 +141,15 @@ public class KryoServer implements ServerProvider { if(con == null){ Log.err("Cannot kick unknown player!"); return; + }else{ + Log.info("Kicking connection #{0} / IP: {1}. Reason: {2}", connection, con.address, reason); } KickPacket p = new KickPacket(); p.reason = reason; con.send(p, SendMode.tcp); + Timers.runTask(2f, con::close); } @Override @@ -371,9 +374,21 @@ public class KryoServer implements ServerProvider { connection.sendUDP(object); } }catch (Exception e){ - e.printStackTrace(); + Log.err(e); Log.info("Disconnecting invalid client!"); + try{ + NetErrorPacket packet = new NetErrorPacket(); + packet.message = Strings.parseException(e, true); + Timers.runTask(5f, connection::close); + }catch (Exception e2){ + Log.err(e2); + connection.close(); + } connection.close(); + + KryoConnection k = getByKryoID(connection.getID()); + if(k != null) connections.remove(k); + Log.info("Connection removed {0}", k); } } } @@ -398,7 +413,7 @@ public class KryoServer implements ServerProvider { @Override public void onOpen(WebSocket conn, ClientHandshake handshake) { Connect connect = new Connect(); - connect.addressTCP = conn.getRemoteSocketAddress().toString(); + connect.addressTCP = conn.getRemoteSocketAddress().getAddress().getHostAddress(); KryoConnection kn = new KryoConnection(lastconnection ++, connect.addressTCP, conn); Log.info("&bRecieved web connection: {0} {1}", kn.id, connect.addressTCP); diff --git a/server/src/io/anuke/mindustry/server/ServerControl.java b/server/src/io/anuke/mindustry/server/ServerControl.java index ab51272464..2b3c172004 100644 --- a/server/src/io/anuke/mindustry/server/ServerControl.java +++ b/server/src/io/anuke/mindustry/server/ServerControl.java @@ -13,6 +13,7 @@ import io.anuke.mindustry.net.Net; import io.anuke.mindustry.net.NetEvents; import io.anuke.mindustry.net.Packets.ChatPacket; import io.anuke.mindustry.net.Packets.KickReason; +import io.anuke.mindustry.net.TraceInfo; import io.anuke.mindustry.ui.fragments.DebugFragment; import io.anuke.mindustry.world.Map; import io.anuke.mindustry.world.Tile; @@ -38,7 +39,12 @@ public class ServerControl extends Module { private ShuffleMode mode; public ServerControl(){ - Settings.defaults("shufflemode", "normal"); + Settings.defaultList( + "shufflemode", "normal", + "bans", "", + "admins", "" + ); + mode = ShuffleMode.valueOf(Settings.getString("shufflemode")); Effects.setScreenShakeProvider((a, b) -> {}); @@ -99,7 +105,7 @@ public class ServerControl extends Module { handler.register("help", "Displays this command list.", arg -> { info("Commands:"); for(Command command : handler.getCommandList()){ - print(" &y" + command.text + (command.params.isEmpty() ? "" : " ") + command.params + " - &lm" + command.description); + print(" &y" + command.text + (command.paramText.isEmpty() ? "" : " ") + command.paramText + " - &lm" + command.description); } }); @@ -184,7 +190,22 @@ public class ServerControl extends Module { } }); - handler.register("say", "", "Send a message to all players.", arg -> { + handler.register("players", "Display player info.", arg -> { + if(state.is(State.menu)){ + info("&lyServer is closed."); + }else{ + if(playerGroup.size() > 0) { + info("&lyPlayers: {0}", playerGroup.size()); + for (Player p : playerGroup.all()) { + print(" &y{0} / Connection {1} / IP: {2}", p.name, p.clientid, Net.getConnection(p.clientid).address); + } + }else{ + info("&lyNo players connected."); + } + } + }); + + handler.register("say", "", "Send a message to all players.", arg -> { if(!state.is(State.playing)) { err("Not hosting. Host a game first."); return; @@ -192,7 +213,7 @@ public class ServerControl extends Module { netCommon.sendMessage("[GRAY][[Server]:[] " + arg[0]); info("&lyServer: &lb{0}", arg[0]); - }).mergeArgs(); + }); handler.register("difficulty", "", "Set game difficulty.", arg -> { try{ @@ -237,11 +258,6 @@ public class ServerControl extends Module { return; } - if(playerGroup.size() == 0){ - err("But this server is empty. A barren wasteland."); - return; - } - Player target = null; for(Player player : playerGroup.all()){ @@ -259,6 +275,130 @@ public class ServerControl extends Module { } }); + handler.register("ban", "", "Ban a person by name.", arg -> { + if(!state.is(State.playing)) { + err("Can't ban people by name with no players."); + return; + } + + Player target = null; + + for(Player player : playerGroup.all()){ + if(player.name.equalsIgnoreCase(arg[0])){ + target = player; + break; + } + } + + if(target != null){ + String ip = Net.getConnection(player.clientid).address; + netServer.admins.banPlayer(ip); + Net.kickConnection(target.clientid, KickReason.banned); + info("Banned player by IP: {0}", ip); + }else{ + info("Nobody with that name could be found."); + } + }); + + handler.register("bans", "List all banned IPs.", arg -> { + Array bans = netServer.admins.getBanned(); + + if(bans.size == 0){ + Log.info("No banned players have been found."); + }else{ + Log.info("&lyBanned players:"); + for(String string : bans){ + Log.info(" &luy {0} / Last known name: '{1}'", string, netServer.admins.getLastName(string)); + } + } + }); + + handler.register("banip", "", "Ban a person by IP.", arg -> { + if(netServer.admins.banPlayer(arg[0])) { + info("Banned player by IP: {0}.", arg[0]); + + for(Player player : playerGroup.all()){ + if(Net.getConnection(player.clientid).address.equals(arg[0])){ + Net.kickConnection(player.clientid, KickReason.banned); + break; + } + } + }else{ + err("That IP is already banned!"); + } + }); + + handler.register("unbanip", "", "Unban a person by IP.", arg -> { + if(netServer.admins.unbanPlayer(arg[0])) { + info("Unbanned player by IP: {0}.", arg[0]); + }else{ + err("That IP is not banned!"); + } + }); + + handler.register("admin", "", "Make a user admin", arg -> { + if(!state.is(State.playing)) { + err("Open the server first."); + return; + } + + Player target = null; + + for(Player player : playerGroup.all()){ + if(player.name.equalsIgnoreCase(arg[0])){ + target = player; + break; + } + } + + if(target != null){ + String ip = Net.getConnection(player.clientid).address; + netServer.admins.adminPlayer(ip); + NetEvents.handleAdminSet(target, true); + info("Admin-ed player by IP: {0} / {1}", ip, arg[0]); + }else{ + info("Nobody with that name could be found."); + } + }); + + handler.register("unadmin", "", "Removes admin status from a player", arg -> { + if(!state.is(State.playing)) { + err("Open the server first."); + return; + } + + Player target = null; + + for(Player player : playerGroup.all()){ + if(player.name.equalsIgnoreCase(arg[0])){ + target = player; + break; + } + } + + if(target != null){ + String ip = Net.getConnection(player.clientid).address; + netServer.admins.unAdminPlayer(ip); + NetEvents.handleAdminSet(target, false); + info("Un-admin-ed player by IP: {0} / {1}", ip, arg[0]); + }else{ + info("Nobody with that name could be found."); + } + }); + + handler.register("admins", "List all banned IPs.", arg -> { + Array admins = netServer.admins.getAdmins(); + + if(admins.size == 0){ + Log.info("No admins have been found."); + }else{ + Log.info("&lyAdmins:"); + for(String string : admins){ + Log.info(" &luy {0} / Name: '{1}'", string, netServer.admins.getLastName(string)); + } + } + }); + handler.register("runwave", "Trigger the next wave.", arg -> { if(!state.is(State.playing)) { err("Not hosting. Host a game first."); @@ -317,7 +457,7 @@ public class ServerControl extends Module { info(DebugFragment.debugInfo()); }); - handler.register("trace", " ", "Prints debug info about a block", arg -> { + handler.register("traceblock", " ", "Prints debug info about a block", arg -> { try{ int x = Integer.parseInt(arg[0]); int y = Integer.parseInt(arg[1]); @@ -343,6 +483,39 @@ public class ServerControl extends Module { Log.err("Invalid coordinates passed."); } }); + + handler.register("trace", "", "Trace a player's actions", arg -> { + if(!state.is(State.playing)) { + err("Open the server first."); + return; + } + + Player target = null; + + for(Player player : playerGroup.all()){ + if(player.name.equalsIgnoreCase(arg[0])){ + target = player; + break; + } + } + + if(target != null){ + TraceInfo info = netServer.admins.getTrace(Net.getConnection(target.clientid).address); + Log.info("&lcTrace info for player '{0}':", target.name); + Log.info(" &lyEntity ID: {0}", info. playerid); + Log.info(" &lyIP: {0}", info.ip); + Log.info(" &lycustom client: {0}", info.modclient); + Log.info(""); + Log.info(" &lytotal blocks broken: {0}", info.totalBlocksBroken); + Log.info(" &lystructure blocks broken: {0}", info.structureBlocksBroken); + Log.info(" &lylast block broken: {0}", info.lastBlockBroken.formalName); + Log.info(""); + Log.info(" &lytotal blocks placed: {0}", info.totalBlocksPlaced); + Log.info(" &lylast block placed: {0}", info.lastBlockPlaced.formalName); + }else{ + info("Nobody with that name could be found."); + } + }); } private void readCommands(){ @@ -355,8 +528,10 @@ public class ServerControl extends Module { if (response.type == ResponseType.unknownCommand) { err("Invalid command. Type 'help' for help."); - } else if (response.type == ResponseType.invalidArguments) { - err("Invalid command arguments. Usage: " + response.command.text + " " + response.command.params); + }else if (response.type == ResponseType.fewArguments) { + err("Too few command arguments. Usage: " + response.command.text + " " + response.command.paramText); + }else if (response.type == ResponseType.manyArguments) { + err("Too many command arguments. Usage: " + response.command.text + " " + response.command.paramText); } }); }