Merge branches 'lights' and 'master' of https://github.com/Anuken/Mindustry into lights

This commit is contained in:
Anuken 2019-10-03 22:32:53 -04:00
commit 8cd013d2ab
184 changed files with 13512 additions and 8739 deletions

View file

@ -6,11 +6,11 @@
A sandbox tower defense game written in Java.
_[Trello Board](https://trello.com/b/aE2tcUwF/mindustry-40-plans)_
_[Wiki](http://mindustry.wikia.com/wiki/Mindustry_Wiki)_
_[Wiki](https://mindustrygame.github.io/wiki)_
### Building
Bleeding-edge live builds are generated automatically for every commit. You can see them [here](https://jenkins.hellomouse.net/job/mindustry/).
Bleeding-edge live builds are generated automatically for every commit. You can see them [here](https://github.com/Anuken/MindustryBuilds/releases). Old builds might still be on [jenkins](https://jenkins.hellomouse.net/job/mindustry/).
If you'd rather compile on your own, follow these instructions.
First, make sure you have Java 8 and JDK 8 installed. Open a terminal in the root directory, `cd` to the Mindustry folder and run the following commands:

View file

@ -22,20 +22,6 @@ public class Annotations{
public @interface OverrideCallSuper {
}
/** Indicates that a method return or field can be null.*/
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface Nullable{
}
/** Indicates that a method return or field cannot be null.*/
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER, ElementType.LOCAL_VARIABLE})
@Retention(RetentionPolicy.SOURCE)
public @interface NonNull{
}
/** Marks a class as serializable. */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)

View file

@ -139,7 +139,7 @@ public class AssetsAnnotationProcessor extends AbstractProcessor{
loadBegin.addStatement("io.anuke.arc.Core.assets.load("+filename +", "+rtype+".class).loaded = a -> " + name + " = ("+rtype+")a", filepath, filepath.replace(".ogg", ".mp3"));
dispose.addStatement(name + ".dispose()");
dispose.addStatement("io.anuke.arc.Core.assets.unload(" + filename + ")");
dispose.addStatement(name + " = null");
type.addField(FieldSpec.builder(ClassName.bestGuess(rtype), name, Modifier.STATIC, Modifier.PUBLIC).initializer("new io.anuke.arc.audio.mock.Mock" + rtype.substring(rtype.lastIndexOf(".") + 1)+ "()").build());
});

View file

@ -1,32 +1,29 @@
package io.anuke.annotations;
import com.sun.source.util.TreePath;
import com.sun.source.util.Trees;
import com.sun.tools.javac.tree.JCTree;
import com.sun.tools.javac.tree.JCTree.JCExpressionStatement;
import io.anuke.annotations.Annotations.OverrideCallSuper;
import com.sun.source.util.*;
import com.sun.tools.javac.tree.*;
import com.sun.tools.javac.tree.JCTree.*;
import io.anuke.annotations.Annotations.*;
import javax.annotation.processing.*;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.tools.Diagnostic.Kind;
import java.util.List;
import java.util.Set;
import javax.lang.model.*;
import javax.lang.model.element.*;
import javax.tools.Diagnostic.*;
import java.util.*;
@SupportedAnnotationTypes("java.lang.Override")
@SupportedAnnotationTypes({"java.lang.Override"})
public class CallSuperAnnotationProcessor extends AbstractProcessor{
private Trees trees;
@Override
public void init (ProcessingEnvironment pe) {
public void init(ProcessingEnvironment pe){
super.init(pe);
trees = Trees.instance(pe);
}
public boolean process (Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (Element e : roundEnv.getElementsAnnotatedWith(Override.class)) {
if (e.getAnnotation(OverrideCallSuper.class) != null) return false;
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv){
for(Element e : roundEnv.getElementsAnnotatedWith(Override.class)){
if(e.getAnnotation(OverrideCallSuper.class) != null) return false;
CodeAnalyzerTreeScanner codeScanner = new CodeAnalyzerTreeScanner();
codeScanner.setMethodName(e.getSimpleName().toString());
@ -34,10 +31,10 @@ public class CallSuperAnnotationProcessor extends AbstractProcessor{
TreePath tp = trees.getPath(e.getEnclosingElement());
codeScanner.scan(tp, trees);
if (codeScanner.isCallSuperUsed()) {
if(codeScanner.isCallSuperUsed()){
List list = codeScanner.getMethod().getBody().getStatements();
if (!doesCallSuper(list, codeScanner.getMethodName())) {
if(!doesCallSuper(list, codeScanner.getMethodName())){
processingEnv.getMessager().printMessage(Kind.ERROR, "Overriding method '" + codeScanner.getMethodName() + "' must explicitly call super method from its parent class.", e);
}
}
@ -46,12 +43,12 @@ public class CallSuperAnnotationProcessor extends AbstractProcessor{
return false;
}
private boolean doesCallSuper (List list, String methodName) {
for (Object object : list) {
if (object instanceof JCTree.JCExpressionStatement) {
JCTree.JCExpressionStatement expr = (JCExpressionStatement) object;
private boolean doesCallSuper(List list, String methodName){
for(Object object : list){
if(object instanceof JCTree.JCExpressionStatement){
JCTree.JCExpressionStatement expr = (JCExpressionStatement)object;
String exprString = expr.toString();
if (exprString.startsWith("super." + methodName) && exprString.endsWith(");")) return true;
if(exprString.startsWith("super." + methodName) && exprString.endsWith(");")) return true;
}
}
@ -59,7 +56,7 @@ public class CallSuperAnnotationProcessor extends AbstractProcessor{
}
@Override
public SourceVersion getSupportedSourceVersion () {
public SourceVersion getSupportedSourceVersion(){
return SourceVersion.RELEASE_8;
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

View file

@ -22,6 +22,7 @@ load.map = Maps
load.image = Images
load.content = Content
load.system = System
load.mod = Mods
stat.wave = Waves Defeated:[accent] {0}
stat.enemiesDestroyed = Enemies Destroyed:[accent] {0}
@ -32,6 +33,7 @@ stat.delivered = Resources Launched:
stat.rank = Final Rank: [accent]{0}
launcheditems = [accent]Launched Items
launchinfo = [unlaunched][[LAUNCH] your core to obtain the items indicated in blue.
map.delete = Are you sure you want to delete the map "[accent]{0}[]"?
level.highscore = High Score: [accent]{0}
level.select = Level Select
@ -64,6 +66,24 @@ uploadingpreviewfile = Uploading Preview File
committingchanges = Comitting Changes
done = Done
mods.alphainfo = Keep in mind that mods are in alpha, and[scarlet] may be very buggy[].\nReport any issues you find to the Mindustry Github or Discord.
mods.alpha = [accent](Alpha)
mods = Mods
mods.none = [LIGHT_GRAY]No mods found!
mods.guide = Modding Guide
mods.report = Report Bug
mod.enabled = [lightgray]Enabled
mod.disabled = [scarlet]Disabled
mod.disable = Disable
mod.enable = Enable
mod.requiresrestart = The game will now close to apply the mod changes.
mod.reloadrequired = [scarlet]Reload Required
mod.import = Import Mod
mod.import.github = Import Github Mod
mod.remove.confirm = This mod will be deleted.
mod.author = [LIGHT_GRAY]Author:[] {0}
mod.missing = This save contains mods that you have recently updated or no longer have installed. Save corruption may occur. Are you sure you want to load it?\n[lightgray]Mods:\n{0}
about.button = About
name = Name:
noname = Pick a[accent] player name[] first.
@ -164,7 +184,7 @@ save.rename.text = New name:
selectslot = Select a save.
slot = [accent]Slot {0}
editmessage = Edit Message
save.corrupted = [accent]Save file corrupted or invalid!\nIf you have just updated your game, this is probably a change in the save format and [scarlet]not[] a bug.
save.corrupted = Save file corrupted or invalid!
empty = <empty>
on = On
off = Off
@ -178,6 +198,7 @@ warning = Warning.
confirm = Confirm
delete = Delete
view.workshop = View In Workshop
workshop.listing = Edit Workshop Listing
ok = OK
open = Open
customize = Customize Rules
@ -195,6 +216,7 @@ classic.export.text = [accent]Mindustry[] has just had a major update.\nClassic
quit.confirm = Are you sure you want to quit?
quit.confirm.tutorial = Are you sure you know what you're doing?\nThe tutorial can be re-taken in[accent] Settings->Game->Re-Take Tutorial.[]
loading = [accent]Loading...
reloading = [accent]Reloading Mods...
saving = [accent]Saving...
wave = [accent]Wave {0}
wave.waiting = [lightgray]Wave in {0}
@ -215,7 +237,12 @@ map.nospawn.pvp = This map does not have any enemy cores for player to spawn int
map.nospawn.attack = This map does not have any enemy cores for player to attack! Add[SCARLET] red[] cores to this map in the editor.
map.invalid = Error loading map: corrupted or invalid map file.
map.publish.error = Error publishing map: {0}
map.update = Update Map
map.load.error = Error fetching workshop details: {0}
map.missing = This map has been deleted or moved.\n[lightgray]The workshop listing has now been automatically un-linked from the map.
map.publish.confirm = Are you sure you want to publish this map?\n\n[lightgray]Make sure you agree to the Workshop EULA first, or your maps will not show up!
map.menu = Select what you would like to do with this map.
map.changelog = Changelog (optional):
eula = Steam EULA
map.publish = Map published.
map.publishing = [accent]Publishing map...
@ -440,8 +467,6 @@ settings.graphics = Graphics
settings.cleardata = Clear Game Data...
settings.clear.confirm = Are you sure you want to clear this data?\nWhat is done cannot be undone!
settings.clearall.confirm = [scarlet]WARNING![]\nThis will clear all data, including saves, maps, unlocks and keybinds.\nOnce you press 'ok' the game will wipe all data and automatically exit.
settings.clearunlocks = Clear Unlocks
settings.clearall = Clear All
paused = [accent]< Paused >
yes = Yes
no = No
@ -609,6 +634,7 @@ keybind.chat.name = Chat
keybind.player_list.name = Player list
keybind.console.name = Console
keybind.rotate.name = Rotate
keybind.rotateplaced.name = Rotate Existing (Hold)
keybind.toggle_menus.name = Toggle menus
keybind.chat_history_prev.name = Chat history prev
keybind.chat_history_next.name = Chat history next
@ -805,7 +831,7 @@ block.lancer.name = Lancer
block.conveyor.name = Conveyor
block.titanium-conveyor.name = Titanium Conveyor
block.armored-conveyor.name = Armored Conveyor
block.armored-conveyor.description = Moves items at the same speed as titanium conveyors, but possesses more armor. Does not accept inputs from the sides from anything but other conveyors.
block.armored-conveyor.description = Moves items at the same speed as titanium conveyors, but possesses more armor. Does not accept inputs from the sides from anything but other conveyor belts.
block.junction.name = Junction
block.router.name = Router
block.distributor.name = Distributor
@ -927,11 +953,11 @@ unit.eradicator.name = Eradicator
unit.lich.name = Lich
unit.reaper.name = Reaper
tutorial.next = [lightgray]<Tap to continue>
tutorial.intro = You have entered the[scarlet] Mindustry Tutorial.[]\nBegin by[accent] mining copper[]. Tap a copper ore vein near your core to do this.\n\n[accent]{0}/{1} copper
tutorial.drill = Mining manually is inefficient.\n[accent]Drills []can mine automatically.\nClick the drill tab in the bottom right.\nSelect the[accent] mechanical drill[]. Place it on a copper vein by clicking.\n[accent]Right-click[] to stop building, and[accent] Hold Ctrl while scrolling[] to zoom in and out.
tutorial.intro = You have entered the[scarlet] Mindustry Tutorial.[]\nUse [[WASD] to move.\n[accent] Hold [[Ctrl] while scrolling[] to zoom in and out.\nBegin by[accent] mining copper[]. Move close to it, then tap a copper ore vein near your core to do this.\n\n[accent]{0}/{1} copper
tutorial.drill = Mining manually is inefficient.\n[accent]Drills []can mine automatically.\nClick the drill tab in the bottom right.\nSelect the[accent] mechanical drill[]. Place it on a copper vein by clicking.\n[accent]Right-click[] to stop building.
tutorial.drill.mobile = Mining manually is inefficient.\n[accent]Drills []can mine automatically.\nTap the drill tab in the bottom right.\nSelect the[accent] mechanical drill[].\nPlace it on a copper vein by tapping, then press the[accent] checkmark[] below to confirm your selection.\nPress the[accent] X button[] to cancel placement.
tutorial.blockinfo = Each block has different stats. Each drill can only mine certain ores.\nTo check a block's info and stats,[accent] tap the "?" button while selecting it in the build menu.[]\n\n[accent]Access the Mechanical Drill's stats now.[]
tutorial.conveyor = [accent]Conveyors[] are used to transport items to the core.\nMake a line of conveyors from the drill to the core.\n[accent]Hold down the mouse to place in a line.[]\nHold[accent] CTRL[] while selecting a line to place diagonally.\n\n[accent]Place 2 conveyors with the line tool, then deliver an item into the core.
tutorial.conveyor = [accent]Conveyors[] are used to transport items to the core.\nMake a line of conveyors from the drill to the core.\n[accent]Hold down the mouse to place in a line.[]\nHold[accent] CTRL[] while selecting a line to place diagonally.\nUse the scrollwheel to rotate blocks before placing them.\n[accent]Place 2 conveyors with the line tool, then deliver an item into the core.
tutorial.conveyor.mobile = [accent]Conveyors[] are used to transport items to the core.\nMake a line of conveyors from the drill to the core.\n[accent] Place in a line by holding down your finger for a few seconds[] and dragging in a direction.\n\n[accent]Place 2 conveyors with the line tool, then deliver an item into the core.
tutorial.turret = Once an item enters your core, it can be used for building.\nKeep in mind that not all items can be used for building.\nItems that are not used for building, such as[accent] coal[] or[accent] scrap[], cannot be put into the core.\nDefensive structures must be built to repel the[lightgray] enemy[].\nBuild a[accent] duo turret[] near your base.
tutorial.drillturret = Duo turrets require[accent] copper ammo []to shoot.\nPlace a drill near the turret.\nLead conveyors into the turret to supply it with copper.\n\n[accent]Ammo delivered: 0/1
@ -945,7 +971,7 @@ tutorial.withdraw = In some situations, taking items directly from blocks is nec
tutorial.deposit = Deposit items into blocks by dragging from your ship to the destination block.\n\n[accent]Deposit your copper back into the core.[]
tutorial.waves = The[lightgray] enemy[] approaches.\n\nDefend the core for 2 waves.[accent] Click[] to shoot.\nBuild more turrets and drills. Mine more copper.
tutorial.waves.mobile = The[lightgray] enemy[] approaches.\n\nDefend the core for 2 waves. Your ship will automatically fire at enemies.\nBuild more turrets and drills. Mine more copper.
tutorial.launch = Once you reach a specific wave, you are able to[accent] launch the core[], leaving your defenses behind and[accent] obtaining all the resources in your core.[]\nThese resources can then be used to research new technology.\n\n[accent]Press the launch button.
tutorial.launch = Once you reach a specific wave, you are able to[accent] launch the core[], leaving your defenses behind and[accent] obtaining all the resources in your core.[]\nThese obtained resources can then be used to research new technology.\n\n[accent]Press the launch button.
item.copper.description = The most basic structural material. Used extensively in all types of blocks.
item.lead.description = A basic starter material. Used extensively in electronics and liquid transportation blocks.
@ -1071,7 +1097,7 @@ block.core-foundation.description = The second version of the core. Better armor
block.core-nucleus.description = The third and final iteration of the core capsule. Extremely well armored. Stores massive amounts of resources.
block.vault.description = Stores a large amount of items of each type. An unloader block can be used to retrieve items from the vault.
block.container.description = Stores a small amount of items of each type. An unloader block can be used to retrieve items from the container.
block.unloader.description = Unloads items from a container, vault or core onto a conveyor or directly into an adjacent block. The type of item to be unloaded can be changed by tapping.
block.unloader.description = Unloads items from any nearby non-transportation block. The type of item to be unloaded can be changed by tapping.
block.launch-pad.description = Launches batches of items without any need for a core launch.
block.launch-pad-large.description = An improved version of the launch pad. Stores more items. Launches more frequently.
block.duo.description = A small, cheap turret. Useful against ground units.

View file

@ -1014,7 +1014,7 @@ block.router.description = Akzeptiert Materialien aus einer Richtung und leitet
block.distributor.description = Ein weiterentwickelter Verteiler, der Materialien in bis zu sieben Richtungen gleichmäßig verteilt.
block.overflow-gate.description = Ein Verteiler, der nur Materialien nach links oder rechts ausgibt, falls der Weg gerade aus blockiert ist.
block.mass-driver.description = Ultimativer Transportblock. Sammelt mehrere Materialien und schießt sie zu einem verbundenen Massenbeschleuniger über eine große Reichweite.
block.mechanical-pump.description = Eine günstige, langsame Pumpe, die keine Strom benötigt.
block.mechanical-pump.description = Eine günstige, langsame Pumpe, die keinen Strom benötigt.
block.rotary-pump.description = Eine fortgeschrittene Pumpe, die mithilfe von Strom doppelt so schnell pumpt.
block.thermal-pump.description = Die ultimative Pumpe, dreimal so schnell wie eine mechanische Pumpe und die einzige Pumpe, die Lava fördern kann.
block.conduit.description = Standard Flüssigkeits-Transportblock. Funktioniert wie ein Förderband, nur für Flüssigkeiten. Wird am Besten mit Extraktoren, Pumpen oder anderen Kanälen benutzt.

View file

@ -646,7 +646,7 @@ item.coal.name = Carbón
item.graphite.name = Grafito
item.titanium.name = Titanio
item.thorium.name = Torio
item.silicon.name = Silicona
item.silicon.name = Silicio
item.plastanium.name = Plastanio
item.phase-fabric.name = Tejido de fase
item.surge-alloy.name = Aleación Eléctrica
@ -792,7 +792,7 @@ block.distributor.name = Distribuidor
block.sorter.name = Clasificador
block.message.name = Message
block.overflow-gate.name = Compuerta de Desborde
block.silicon-smelter.name = Horno para Silicona
block.silicon-smelter.name = Horno para Silicio
block.phase-weaver.name = Tejedor de Fase
block.pulverizer.name = Pulverizador
block.cryofluidmixer.name = Mezclador de Criogénicos
@ -968,11 +968,11 @@ unit.revenant.description = Una unidad aérea pesada con misiles.
block.message.description = Stores a message. Used for communication between allies.
block.graphite-press.description = Comprime carbón en piezas de grafito puro.
block.multi-press.description = Una versión mejorada de la prensa de grafito. Utiliza agua y energía para procesar carbón rápida y eficientemente.
block.silicon-smelter.description = Reduce arena con coque de alta pureza para producir silicona.
block.silicon-smelter.description = Reduce la arena con carbón puro. Produce silicio.
block.kiln.description = Funde arena y plomo en metacristal. Requiere cantidades pequeñas de energía.
block.plastanium-compressor.description = Produce plastanio con aceite y titanio.
block.phase-weaver.description = Produce tejido de fase del torio radioactivo y altas cantidades de arena.
block.alloy-smelter.description = Produce "surge alloy" con titanio, plomo, silicona y cobre.
block.alloy-smelter.description = Produce "surge alloy" con titanio, plomo, silicio y cobre.
block.cryofluidmixer.description = Combina agua y titanio en líquido criogénico, que es mucho más eficiente para enfriar.
block.blast-mixer.description = Usa aceite para transformar pirotita en un objeto menos inflamable pero más explosivo: compuesto explosivo.
block.pyratite-mixer.description = Mezcla carbón, plomo y arena en pirotita altamente inflamable.
@ -999,7 +999,7 @@ block.surge-wall.description = El bloque defensivo más fuerte.\nTiene una peque
block.surge-wall-large.description = El bloque defensivo más fuerte.\nTiene una pequeña probabilidad de disparar rayos al atacante.\nOcupa múltiplies casillas.
block.door.description = Una puerta pequeña que puede ser abierta y cerrada tocándola.\nSi está abirta, los enemigos pueden moverse y disparar a través de ella.
block.door-large.description = Una puerta grande que puede ser abierta y cerrada tocándola.\nSi está abirta, los enemigos pueden moverse y disparar a través de ella.\nOcupa múltiples casillas.
block.mender.description = Repara bloques cercanos periódicamente. Mantiene a las defensas reparadas entre oleadas.Puede usar silicona opcionalmente para mejorar el alcance y la eficiencia.
block.mender.description = Repara bloques cercanos periódicamente. Mantiene a las defensas reparadas entre oleadas. Puede usar silicio opcionalmente para mejorar el alcance y la eficiencia.
block.mend-projector.description = Regenera edificios cercanos periódcamente.
block.overdrive-projector.description = Aumenta la velocidad de edificios cercanos como taladros y transportadores.
block.force-projector.description = Crea un área de fuerza hexagonal alrededor de él, protegiendo edificios y unidades dentro de él del daño de las balas.

File diff suppressed because it is too large Load diff

View file

@ -48,7 +48,7 @@ minimap = Мінімапа
close = Закрити
website = Веб-сайт
quit = Вихід
save.quit = Save & Quit
save.quit = Зберегти & Вийти
maps = Мапи
maps.browse = Перегляд мап
continue = Продовжити
@ -98,7 +98,7 @@ host = Сервер
hosting = [accent]Відкриття сервера…
hosts.refresh = Оновити
hosts.discovering = Пошук локальних ігор
hosts.discovering.any = Discovering games
hosts.discovering.any = Пошук ігор
server.refreshing = Оновлення сервера
hosts.none = [lightgray]Локальних ігр не знайдено
host.invalid = [scarlet]Не вдалося підключитися до сервера.
@ -122,16 +122,16 @@ server.version = [lightgray]Версія: {0}
server.custombuild = [yellow]Користувацька збірка
confirmban = Ви дійсно хочете заблокувати цього гравця?
confirmkick = Ви дійсно хочете викинути цього гравця?
confirmvotekick = Are you sure you want to vote-kick this player?
confirmvotekick = Ви дійсно хочете вигнати цього гравця за допомогою голосуванняr?
confirmunban = Ви дійсно хочете розблокувати цього гравця?
confirmadmin = Ви дійсно хочете зробити цього гравця адміністратором?
confirmunadmin = Ви дійсно хочете видалити статус адміністратора з цього гравця?
joingame.title = Приєднатися до гри
joingame.ip = IP:
disconnect = Відключено.
disconnect.error = Connection error.
disconnect.closed = Connection closed.
disconnect.timeout = Timed out.
disconnect.error = Помилка з’єднання.
disconnect.closed = З'єднання закрито.
disconnect.timeout = Час вийшов.
disconnect.data = Не вдалося завантажити дані світу!
cantconnect = Не вдалося під’єднатися до гри ([accent]{0}[]).
connecting = [accent]Підключення…
@ -159,7 +159,7 @@ save.rename = Перейменувати
save.rename.text = Нова назва:
selectslot = Виберіть збереження.
slot = [accent]Слот {0}
editmessage = Edit Message
editmessage = Редагувати повідомлення
save.corrupted = [accent]Збережений файл пошкоджено або недійсний! \nЯкщо ви щойно оновили свою гру, це, мабуть, є зміною формату збереження та [scarlet] не є[] помилкою.
empty = <Порожньо>
on = Увімкнено
@ -167,13 +167,14 @@ off = Вимкнено
save.autosave = Автозбереження: {0}
save.map = Мапа: {0}
save.wave = Хвиля {0}
save.mode = Gamemode: {0}
save.mode = Режим гри: {0}
save.date = Останнє збереження
save.playtime = Час гри: {0}
warning = Попередження
confirm = Підтвердження
delete = Видалити
view.workshop = Переглянути в Майстерні
workshop.listing = Редагувати список Майстерні
ok = ОК
open = Відкрити
customize = Налаштувати правила
@ -211,10 +212,15 @@ map.nospawn.pvp = У цієї мапи немає ворожих ядер, в я
map.nospawn.attack = У цієї мапи немає ворожих ядер, в яких гравець може з’явитися! Додайте [SCARLET]червоне[] ядро до цієї мапи в редакторі.
map.invalid = Помилка завантаження мапи: пошкоджений або невірний файл мапи.
map.publish.error = Помилка при опублікуванні мапи: {0}
map.update = Оновити мапу
map.load.error = Помилка отримання даних з Майстерню: {0}
map.missing = Цю карту було видалено або переміщено.\n[lightgray]Перелік у Майстерні автоматично від’єднано від мапи.
map.menu = Виберіть, що ви хочете зробити з цією мапою.
map.changelog = Список змік (необов’язково):
map.publish.confirm = Ви дійсно хочете опублікувати цю мапу?\n\n[lightgray]Переконайтеся, що спершу ви згодні з Ліцензійною угодою Steam, або ваші мапи не з’являться!
eula = Ліцензійна угода
map.publish = Map published.
map.publishing = [accent]Publishing map...
map.publish = Мапа опублікована.
map.publishing = [accent]Публікації мапи...
editor.brush = Пензлик
editor.openin = Відкрити в редакторі
editor.oregen = Генерація руд
@ -222,7 +228,7 @@ editor.oregen.info = Генерація руд:
editor.mapinfo = Інформація про мапу
editor.author = Автор:
editor.description = Опис:
editor.nodescription = A map must have a description of at least 4 characters before being published.
editor.nodescription = Мапа повинна мати щонаймеше 4 символи для публікації.
editor.waves = Хвилі:
editor.rules = Правила:
editor.generation = Генерація:
@ -246,7 +252,7 @@ waves.invalid = Недійсні хвилі у буфері обміну.
waves.copied = Хвилі скопійовані.
waves.none = Вороги не були встановлені.\nЗазначимо, що пусті хвилі будуть автоматично замінені звичайною хвилею.
editor.default = [lightgray]<За замовчуванням>
details = Details...
details = Деталі...
edit = Редагувати…
editor.name = Назва:
editor.spawn = Створити бойову одиницю
@ -256,7 +262,7 @@ editor.errorload = Помилка завантаження зображення:
editor.errorsave = Помилка збереження зображення:\n[accent]{0}
editor.errorimage = Це зображення, а не мапа. Не змінюйте розширення, очікуючи, що це запрацює.\n\nЯкщо Ви хочете імпортувати застарілку мапу, то використовуйте кнопку «Імпортувати застаріле зображення» у редакторі.
editor.errorlegacy = Ця мапа занадто стара і використовує попередній формат мапи, який більше не підтримується.
editor.errornot = This is not a map file.
editor.errornot = Це не мапа.
editor.errorheader = Цей файл мапи недійсний або пошкоджений.
editor.errorname = Мапа не має імені. Може Ви намагаєтеся завантажити збереження?
editor.update = Оновити
@ -289,7 +295,7 @@ editor.resizemap = Змінити розмір мапи
editor.mapname = Назва мапи:
editor.overwrite = [accent]Попередження!\nЦе перезаписує існуючу мапу.
editor.overwrite.confirm = [scarlet]Попередження![] Мапа з такою назвою вже існує. Ви впевнені, що хочете переписати її?
editor.exists = A map with this name already exists.
editor.exists = Мапа за такою назвою вже існує.
editor.selectmap = Виберіть мапу для завантаження:
toolmode.replace = Замінити
toolmode.replace.description = Малює тільки\nна суцільних блоках.
@ -369,7 +375,7 @@ launch.skip.confirm = Якщо Ви пропустите зараз, Ви не
uncover = Розкрити
configure = Вивантажити конфігурацію
configure.locked = [lightgray]Можливість розблокувати вивантаження ресурсів буде доступна на {0}-тій хвилі.
configure.invalid = Amount must be a number between 0 and {0}.
configure.invalid = Кількість повинна бути числом між 0 та {0}.
zone.unlocked = Зона «[lightgray]{0}» тепер розблокована.
zone.requirement.complete = Ви досягли {0}-тої хвилі,\nВимоги до зони «{1}» виконані.
zone.config.complete = Ви досягли {0}-тої хвилі.\nМожливість вивантаження ресурсів тепер розблокована.
@ -466,7 +472,7 @@ blocks.boosteffect = Прискорювальний ефект
blocks.maxunits = Максимальна кількість активних одиниць
blocks.health = Здоров’я
blocks.buildtime = Час будівництва
blocks.buildcost = Build Cost
blocks.buildcost = Вартість будування
blocks.inaccuracy = Розкид
blocks.shots = Постріли
blocks.reload = Постріли/секунду
@ -475,7 +481,7 @@ bar.drilltierreq = Потребується кращий бур
bar.drillspeed = Швидкість буріння: {0}/с
bar.efficiency = Ефективність: {0}%
bar.powerbalance = Енергія: {0}/с
bar.powerstored = Stored: {0}/{1}
bar.powerstored = Зберігає: {0}/{1}
bar.poweramount = Енергія: {0}
bar.poweroutput = Вихідна енергія: {0}
bar.items = Предмети: {0}
@ -524,7 +530,7 @@ setting.antialias.name = Згладжування[lightgray] (потребує
setting.indicators.name = Показувати у сторону ворогів та союзників
setting.autotarget.name = Авто-стрільба
setting.keyboard.name = Миш+Керування з клавіатури
setting.touchscreen.name = Touchscreen Controls
setting.touchscreen.name = Керування сенсорним екраном
setting.fpscap.name = Максимальний FPS
setting.fpscap.none = Необмежений
setting.fpscap.text = {0} FPS
@ -534,7 +540,7 @@ setting.difficulty.training = Навчання
setting.difficulty.easy = Легка
setting.difficulty.normal = Нормальна
setting.difficulty.hard = Важка
setting.difficulty.insane = Зачистка
setting.difficulty.insane = Неможлива
setting.difficulty.name = Складність:
setting.screenshake.name = Тряска екрану
setting.effects.name = Ефекти
@ -555,9 +561,10 @@ setting.sfxvol.name = Гучність звукових ефектів
setting.mutesound.name = Заглушити звук
setting.crashreport.name = Відсилати анонімні звіти про аварійне завершення гри
setting.savecreate.name = Автоматичне створення збережень
setting.publichost.name = Public Game Visibility
setting.publichost.name = Загальнодоступність гри
setting.chatopacity.name = Непрозорість чату
setting.playerchat.name = Відображати хмару чата над гравцями
public.confirm = Ви хочете зробити цю гру загальнодоступною?\n[lightgray]Це можна змінити у Налаштування->Гра->Public Game Visibility.
uiscale.reset = Масштаб користувальницького інтерфейсу було змінено.\nНатисніть «ОК» для підтверждення цього масшатабу.\n[scarlet]Повернення налаштувань і вихід через[accent] {0}[] …
uiscale.cancel = Скасувати & Вийти
setting.bloom.name = Світіння
@ -567,7 +574,7 @@ category.general.name = Основне
category.view.name = Перегляд
category.multiplayer.name = Мережева гра
command.attack = Атакувати
command.rally = Rally
command.rally = Точка збору
command.retreat = Відступити
keybind.gridMode.name = Вибрати блок
keybind.gridModeShift.name = Вибрати категорію
@ -604,6 +611,7 @@ mode.survival.name = Хвилі
mode.survival.description = Звичайний режим. В цьому режимі треба самим добувати ресурси та хвилі йдуть автоматично.\n[gray]Потребуються точки появи ворогів для гри.
mode.sandbox.name = Пісочниця
mode.sandbox.description = В режимі «Пісочниця» незкінченні ресурси(але їх все одно можно добувати) та хвилі йдуть за вашим бажанням.
mode.editor.name = Редактор
mode.pvp.name = PVP
mode.pvp.description = боріться проти інших гравців.\n[gray]Для гри потрібно принаймні 2 ядра різного кольору на мапі.
mode.attack.name = Атака
@ -784,13 +792,13 @@ block.hail.name = Град
block.lancer.name = Списоносець
block.conveyor.name = Конвеєр
block.titanium-conveyor.name = Титановий конвеєр
block.armored-conveyor.name = Armored Conveyor
block.armored-conveyor.description = Moves items at the same speed as titanium conveyors, but possesses more armor. Does not accept inputs from the sides from anything but other conveyors.
block.armored-conveyor.name = Броньований конвеєр
block.armored-conveyor.description = Переміщує предмети з тією ж швидкістю, як і титанові конвеєри, але має більше міцності. Не приймає введення з боків ні з чого, крім інших конвеєрів.
block.junction.name = Перехрестя
block.router.name = Маршрутизатор
block.distributor.name = Розподілювач
block.sorter.name = Сортувальник
block.message.name = Message
block.message.name = Повідомлення
block.overflow-gate.name = Надмірний затвор
block.silicon-smelter.name = Кремнієвий плавильний завод
block.phase-weaver.name = Фазовий ткач
@ -879,8 +887,8 @@ block.overdrive-projector.name = Сверхприводний проектор
block.force-projector.name = Силовий проектор
block.arc.name = Дуга
block.rtg-generator.name = Радіоізотопний термоелектричний генератор
block.spectre.name = Мара
block.meltdown.name = Катастрофа
block.spectre.name = Спектр
block.meltdown.name = Випалювач
block.container.name = Склад
block.launch-pad.name = Стартовий майданчик
block.launch-pad-large.name = Великий стартовий майданчик
@ -892,7 +900,7 @@ team.derelict.name = Залишена
team.green.name = Зелена
team.purple.name = Фіолетова
unit.spirit.name = Ремонтувальний дрон «Привид»
unit.draug.name = Draug Miner Drone
unit.draug.name = Добувний дрон «Драугр»
unit.phantom.name = Будівельний дрон «Фантом»
unit.dagger.name = Кинджал
unit.crawler.name = Камікадзе
@ -907,7 +915,7 @@ unit.eradicator.name = Викорінювач
unit.lich.name = Лич
unit.reaper.name = Жнець
tutorial.next = [lightgray]<Натисніть для продовження>
tutorial.intro = Ви розпочали[scarlet] навчання по Mindustry.[]\nРозпочність з[accent] видобування міді[]. Натисніть на мідну жилу біля вашого ядра, щоб зробити це.\n\n[accent]{0}/{1} міді
tutorial.intro = Ви розпочали[scarlet] навчання по Mindustry.[]\nРозпочність з[accent] видобування міді[]. Використовуйте [[WASD] для руху, а потім натисність на мідну жилу біля вашого ядра, щоб зробити це.\n\n[accent]{0}/{1} міді
tutorial.drill = Добування вручну неефективне.\n[accent]Бури []можуть добувати автоматично.\nНатисніть на вкладку свердла знизу зправа.\nВиберіть[accent] механічний бур[]. Розмістіть його на мідній жилі натисканням.\n[accent]Натисніть ПКМ[], щоб зупинити будування.
tutorial.drill.mobile = Добування вручну неефективне.\n[accent]Бури []можуть добувати автоматично.\nНатисність на вкладку сведла знизу зправа.\nВиберіть[accent] механічний бур[]. Розмістіть його на мідній жилі натисканням, потім натисність на [accent] галочку[] нижче, щоб підтвердити розміщення to confirm your selection.\nPress the[accent] X button[] to cancel placement.
tutorial.blockinfo = Кожен блок має різні характеристики. Кожний бур може видобувати тільки певні руди.\nЩоб переглянути інформацію та характеристики блока,[accent] натисність на кнопку «?», коли Ви вибрали блок у меню будування.[]\n\n[accent]Перегляньте характеристику Механічного бура прямо зараз.[]
@ -965,7 +973,7 @@ unit.eruptor.description = Важкий мех, призначеней для з
unit.wraith.description = Швидкий перехоплювач, який використовується для тактики «атакуй і біжи». Пріоритет — енергетичні генератори.
unit.ghoul.description = Важкий килимовий бомбардувальник. Пробиває ворожі структури, орієнтуючись на віжливу інфраструктуру.
unit.revenant.description = Важкий ракетний масив.
block.message.description = Stores a message. Used for communication between allies.
block.message.description = Зберігає повідомлення. Використовується для комунікаціх між союзниками.
block.graphite-press.description = Стискає шматки вугілля в чисті аркуші графіту.
block.multi-press.description = Модернізована версія графітового преса. Використовує воду та енергію для швидкої та ефективної переробки вугілля.
block.silicon-smelter.description = Змішує пісок з чистим вугіллям. Виробляє кремній.
@ -1050,7 +1058,7 @@ block.core-foundation.description = Друга версія ядра. Краще
block.core-nucleus.description = Третя і остання ітерація капсули ядра. Надзвичайно добре броньований. Зберігає величезні обсяги ресурсів.
block.vault.description = Зберігає велику кількість предметів кожного типу. Блок розвантажувача може використовуватися для отримання предметів із сховища.
block.container.description = Зберігає велику кількість предметів кожного типу. Блок розвантажувача може використовуватися для отримання предметів із сховища.
block.unloader.description = Вивантажує предмети з контейнера, склепіння або серцевини на конвеєр або безпосередньо в сусідній блок. Тип предмета для завантаження можна змінити, натиснувши на блок.
block.unloader.description = Вивантажує предмети з блока, який не переміщує предмети, на конвеєр або безпосередньо в сусідній блок. Тип предмета для завантаження можна змінити, натиснувши на блок.
block.launch-pad.description = Запускає партії предметів без необхідності запуску ядра.
block.launch-pad-large.description = Покращена версія стартового майданчика. Зберігає більше предметів. Запускається частіше.
block.duo.description = Невелика дешева башта. Корисна проти наземних одиниць.

Binary file not shown.

File diff suppressed because it is too large Load diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 684 KiB

After

Width:  |  Height:  |  Size: 712 KiB

Before After
Before After

Binary file not shown.

Before

Width:  |  Height:  |  Size: 578 KiB

After

Width:  |  Height:  |  Size: 721 KiB

Before After
Before After

View file

@ -2,6 +2,8 @@ package io.anuke.mindustry;
import io.anuke.arc.*;
import io.anuke.arc.assets.*;
import io.anuke.arc.assets.loaders.*;
import io.anuke.arc.audio.*;
import io.anuke.arc.graphics.*;
import io.anuke.arc.graphics.g2d.*;
import io.anuke.arc.math.*;
@ -13,6 +15,7 @@ import io.anuke.mindustry.game.EventType.*;
import io.anuke.mindustry.gen.*;
import io.anuke.mindustry.graphics.*;
import io.anuke.mindustry.maps.*;
import io.anuke.mindustry.mod.*;
import io.anuke.mindustry.net.Net;
import static io.anuke.arc.Core.*;
@ -40,9 +43,15 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform
batch = new SpriteBatch();
assets = new AssetManager();
assets.setLoader(Texture.class, "." + mapExtension, new MapPreviewLoader());
tree = new FileTree();
assets.setLoader(Sound.class, new SoundLoader(tree));
assets.setLoader(Music.class, new MusicLoader(tree));
assets.load("sprites/error.png", Texture.class);
atlas = TextureAtlas.blankAtlas();
Vars.net = new Net(platform.getNet());
mods = new Mods();
UI.loadSystemCursors();
@ -71,6 +80,8 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform
add(netServer = new NetServer());
add(netClient = new NetClient());
assets.load(mods);
assets.loadRun("contentinit", ContentLoader.class, () -> {
content.init();
content.load();
@ -108,6 +119,7 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform
listener.init();
}
super.resize(graphics.getWidth(), graphics.getHeight());
mods.each(Mod::init);
finished = true;
Events.fire(new ClientLoadEvent());
}
@ -182,7 +194,7 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform
if(assets.getCurrentLoading() != null){
String name = assets.getCurrentLoading().fileName.toLowerCase();
String key = name.contains("content") ? "content" : name.contains("msav") || name.contains("maps") ? "map" : name.contains("ogg") || name.contains("mp3") ? "sound" : name.contains("png") ? "image" : "system";
String key = name.contains("content") ? "content" : name.contains("mod") ? "mods" : name.contains("msav") || name.contains("maps") ? "map" : name.contains("ogg") || name.contains("mp3") ? "sound" : name.contains("png") ? "image" : "system";
font.draw(bundle.get("load." + key, ""), graphics.getWidth() / 2f, graphics.getHeight() / 2f - height / 2f - Scl.scl(10f), Align.center);
}
}

View file

@ -18,19 +18,21 @@ import io.anuke.mindustry.game.*;
import io.anuke.mindustry.gen.*;
import io.anuke.mindustry.input.*;
import io.anuke.mindustry.maps.*;
import io.anuke.mindustry.mod.*;
import io.anuke.mindustry.net.Net;
import io.anuke.mindustry.plugin.*;
import io.anuke.mindustry.world.blocks.defense.ForceProjector.*;
import java.nio.charset.*;
import java.util.*;
import static io.anuke.arc.Core.settings;
import static io.anuke.arc.Core.*;
@SuppressWarnings("unchecked")
public class Vars implements Loadable{
/** Whether to load locales.*/
public static boolean loadLocales = true;
/** Maximum number of broken blocks. TODO implement or remove.*/
public static final int maxBrokenBlocks = 256;
/** IO buffer size. */
public static final int bufferSize = 8192;
/** global charset, since Android doesn't support the Charsets class */
@ -43,6 +45,10 @@ public class Vars implements Loadable{
public static final String discordURL = "https://discord.gg/mindustry";
/** URL for sending crash reports to */
public static final String crashReportURL = "http://mins.us.to/report";
/** URL the links to the wiki's modding guide.*/
public static final String modGuideURL = "https://mindustrygame.github.io/wiki/modding/";
/** URL the links to the wiki's modding guide.*/
public static final String reportIssueURL = "https://github.com/Anuken/Mindustry/issues/new?template=bug_report.md";
/** list of built-in servers.*/
public static final Array<String> defaultServers = Array.with(/*"mins.us.to"*/);
/** maximum distance between mine and core that supports automatic transferring */
@ -120,8 +126,8 @@ public class Vars implements Loadable{
public static FileHandle tmpDirectory;
/** data subdirectory used for saves */
public static FileHandle saveDirectory;
/** data subdirectory used for plugins */
public static FileHandle pluginDirectory;
/** data subdirectory used for mods */
public static FileHandle modDirectory;
/** map file extension */
public static final String mapExtension = "msav";
/** save file extension */
@ -130,6 +136,7 @@ public class Vars implements Loadable{
/** list of all locales that can be switched to */
public static Locale[] locales;
public static FileTree tree;
public static Net net;
public static ContentLoader content;
public static GameState state;
@ -138,7 +145,7 @@ public class Vars implements Loadable{
public static DefaultWaves defaultWaves;
public static LoopControl loops;
public static Platform platform = new Platform(){};
public static Plugins plugins;
public static Mods mods;
public static World world;
public static Maps maps;
@ -193,6 +200,9 @@ public class Vars implements Loadable{
Version.init();
if(tree == null) tree = new FileTree();
if(mods == null) mods = new Mods();
content = new ContentLoader();
loops = new LoopControl();
defaultWaves = new DefaultWaves();
@ -240,15 +250,18 @@ public class Vars implements Loadable{
mapPreviewDirectory = dataDirectory.child("previews/");
saveDirectory = dataDirectory.child("saves/");
tmpDirectory = dataDirectory.child("tmp/");
pluginDirectory = dataDirectory.child("plugins/");
modDirectory = dataDirectory.child("mods/");
modDirectory.mkdirs();
mods.load();
maps.load();
}
public static void loadSettings(){
Core.settings.setAppName(appName);
if(steam){
if(steam || "steam".equals(Version.modifier)){
Core.settings.setDataDirectory(Core.files.local("saves/"));
}

View file

@ -6,6 +6,7 @@ import io.anuke.arc.collection.*;
import io.anuke.arc.function.*;
import io.anuke.arc.math.geom.*;
import io.anuke.arc.util.*;
import io.anuke.arc.util.ArcAnnotate.*;
import io.anuke.arc.util.async.*;
import io.anuke.mindustry.game.EventType.*;
import io.anuke.mindustry.game.*;
@ -32,7 +33,8 @@ public class Pathfinder implements Runnable{
/** handles task scheduling on the update thread. */
private TaskQueue queue = new TaskQueue();
/** current pathfinding thread */
private @Nullable Thread thread;
private @Nullable
Thread thread;
public Pathfinder(){
Events.on(WorldLoadEvent.class, event -> {
@ -92,7 +94,11 @@ public class Pathfinder implements Runnable{
int x = tile.x, y = tile.y;
tile.getLinkedTiles(t -> tiles[t.x][t.y] = packTile(t));
tile.getLinkedTiles(t -> {
if(Structs.inBounds(t.x, t.y, tiles)){
tiles[t.x][t.y] = packTile(t);
}
});
//can't iterate through array so use the map, which should not lead to problems
for(PathData[] arr : pathMap){

View file

@ -48,7 +48,7 @@ public class WaveSpawner{
for(SpawnGroup group : state.rules.spawns){
int spawned = group.getUnitsSpawned(state.wave - 1);
if(group.type.isFlying){
if(group.type.flying){
float spread = margin / 1.5f;
eachFlyerSpawn((spawnX, spawnY) -> {

View file

@ -781,7 +781,7 @@ public class Blocks implements ContentList{
}};
copperWallLarge = new Wall("copper-wall-large"){{
requirements(Category.defense, ItemStack.mult(copperWall.buildRequirements, 4));
requirements(Category.defense, ItemStack.mult(copperWall.requirements, 4));
health = 80 * 4 * wallHealthMultiplier;
size = 2;
}};
@ -792,7 +792,7 @@ public class Blocks implements ContentList{
}};
titaniumWallLarge = new Wall("titanium-wall-large"){{
requirements(Category.defense, ItemStack.mult(titaniumWall.buildRequirements, 4));
requirements(Category.defense, ItemStack.mult(titaniumWall.requirements, 4));
health = 110 * wallHealthMultiplier * 4;
size = 2;
}};
@ -803,7 +803,7 @@ public class Blocks implements ContentList{
}};
thoriumWallLarge = new Wall("thorium-wall-large"){{
requirements(Category.defense, ItemStack.mult(thoriumWall.buildRequirements, 4));
requirements(Category.defense, ItemStack.mult(thoriumWall.requirements, 4));
health = 200 * wallHealthMultiplier * 4;
size = 2;
}};
@ -814,7 +814,7 @@ public class Blocks implements ContentList{
}};
phaseWallLarge = new DeflectorWall("phase-wall-large"){{
requirements(Category.defense, ItemStack.mult(phaseWall.buildRequirements, 4));
requirements(Category.defense, ItemStack.mult(phaseWall.requirements, 4));
health = 150 * 4 * wallHealthMultiplier;
size = 2;
}};
@ -825,7 +825,7 @@ public class Blocks implements ContentList{
}};
surgeWallLarge = new SurgeWall("surge-wall-large"){{
requirements(Category.defense, ItemStack.mult(surgeWall.buildRequirements, 4));
requirements(Category.defense, ItemStack.mult(surgeWall.requirements, 4));
health = 230 * 4 * wallHealthMultiplier;
size = 2;
}};
@ -836,7 +836,7 @@ public class Blocks implements ContentList{
}};
doorLarge = new Door("door-large"){{
requirements(Category.defense, ItemStack.mult(door.buildRequirements, 4));
requirements(Category.defense, ItemStack.mult(door.requirements, 4));
openfx = Fx.dooropenlarge;
closefx = Fx.doorcloselarge;
health = 100 * 4 * wallHealthMultiplier;
@ -1315,7 +1315,8 @@ public class Blocks implements ContentList{
requirements(Category.turret, ItemStack.with(Items.copper, 85, Items.lead, 45));
ammo(
Items.scrap, Bullets.flakScrap,
Items.lead, Bullets.flakLead
Items.lead, Bullets.flakLead,
Items.metaglass, Bullets.flakGlass
);
reload = 18f;
range = 170f;
@ -1558,6 +1559,7 @@ public class Blocks implements ContentList{
cyclone = new ItemTurret("cyclone"){{
requirements(Category.turret, ItemStack.with(Items.copper, 200, Items.titanium, 125, Items.plastanium, 80));
ammo(
Items.metaglass, Bullets.flakGlass,
Items.blastCompound, Bullets.flakExplosive,
Items.plastanium, Bullets.flakPlastic,
Items.surgealloy, Bullets.flakSurge
@ -1631,28 +1633,28 @@ public class Blocks implements ContentList{
produceTime = 2500;
size = 2;
maxSpawn = 1;
consumes.power(1.1f);
consumes.power(1.2f);
consumes.items();
}};
spiritFactory = new UnitFactory("spirit-factory"){{
requirements(Category.units, ItemStack.with(Items.metaglass, 45, Items.lead, 55, Items.silicon, 45));
type = UnitTypes.spirit;
produceTime = 3500;
produceTime = 4000;
size = 2;
maxSpawn = 2;
consumes.power(0.80f);
consumes.items(new ItemStack(Items.silicon, 15), new ItemStack(Items.lead, 15));
maxSpawn = 1;
consumes.power(1.2f);
consumes.items(new ItemStack(Items.silicon, 30), new ItemStack(Items.lead, 30));
}};
phantomFactory = new UnitFactory("phantom-factory"){{
requirements(Category.units, ItemStack.with(Items.titanium, 45, Items.thorium, 40, Items.lead, 55, Items.silicon, 105));
requirements(Category.units, ItemStack.with(Items.titanium, 50, Items.thorium, 60, Items.lead, 65, Items.silicon, 105));
type = UnitTypes.phantom;
produceTime = 3650;
produceTime = 4400;
size = 2;
maxSpawn = 2;
consumes.power(2f);
consumes.items(new ItemStack(Items.silicon, 30), new ItemStack(Items.lead, 20), new ItemStack(Items.titanium, 10));
maxSpawn = 1;
consumes.power(2.5f);
consumes.items(new ItemStack(Items.silicon, 50), new ItemStack(Items.lead, 30), new ItemStack(Items.titanium, 20));
}};
commandCenter = new CommandCenter("command-center"){{

View file

@ -8,11 +8,9 @@ import io.anuke.mindustry.entities.*;
import io.anuke.mindustry.entities.bullet.*;
import io.anuke.mindustry.entities.effect.*;
import io.anuke.mindustry.entities.type.*;
import io.anuke.mindustry.entities.type.Bullet;
import io.anuke.mindustry.game.*;
import io.anuke.mindustry.graphics.*;
import io.anuke.mindustry.world.*;
import io.anuke.mindustry.world.blocks.*;
import static io.anuke.mindustry.Vars.*;
@ -23,7 +21,7 @@ public class Bullets implements ContentList{
artilleryDense, arilleryPlastic, artilleryPlasticFrag, artilleryHoming, artlleryIncendiary, artilleryExplosive, artilleryUnit,
//flak
flakScrap, flakLead, flakPlastic, flakExplosive, flakSurge,
flakScrap, flakLead, flakPlastic, flakExplosive, flakSurge, flakGlass, glassFrag,
//missiles
missileExplosive, missileIncendiary, missileSurge, missileJavelin, missileSwarm, missileRevenant,
@ -39,7 +37,7 @@ public class Bullets implements ContentList{
waterShot, cryoShot, slagShot, oilShot,
//environment, misc.
fireball, basicFlame, pyraFlame, driverBolt, healBullet, frag, eruptorShot,
fireball, basicFlame, pyraFlame, driverBolt, healBullet, healBulletBig, frag, eruptorShot,
//bombs
bombExplosive, bombIncendiary, bombOil;
@ -57,7 +55,7 @@ public class Bullets implements ContentList{
splashDamage = 33f;
}};
artilleryPlasticFrag = new BasicBulletType(2.5f, 7, "bullet"){{
artilleryPlasticFrag = new BasicBulletType(2.5f, 10, "bullet"){{
bulletWidth = 10f;
bulletHeight = 12f;
bulletShrink = 1f;
@ -134,6 +132,16 @@ public class Bullets implements ContentList{
frontColor = Pal.bulletYellow;
}};
glassFrag = new BasicBulletType(3f, 6, "bullet"){{
bulletWidth = 5f;
bulletHeight = 12f;
bulletShrink = 1f;
lifetime = 20f;
backColor = Pal.gray;
frontColor = Color.white;
despawnEffect = Fx.none;
}};
flakLead = new FlakBulletType(4.2f, 3){{
lifetime = 60f;
ammoMultiplier = 4f;
@ -157,8 +165,23 @@ public class Bullets implements ContentList{
splashDamageRadius = 24f;
}};
flakGlass = new FlakBulletType(4f, 3){{
lifetime = 70f;
ammoMultiplier = 5f;
shootEffect = Fx.shootSmall;
reloadMultiplier = 0.8f;
bulletWidth = 6f;
bulletHeight = 8f;
hitEffect = Fx.flakExplosion;
splashDamage = 30f;
splashDamageRadius = 26f;
fragBullet = glassFrag;
fragBullets = 6;
}};
flakPlastic = new FlakBulletType(4f, 6){{
splashDamageRadius = 50f;
splashDamage = 25f;
fragBullet = artilleryPlasticFrag;
fragBullets = 6;
hitEffect = Fx.plasticExplosion;
@ -376,43 +399,13 @@ public class Bullets implements ContentList{
statusDuration = 10f;
}};
healBullet = new BulletType(5.2f, 13){
float healPercent = 3f;
healBullet = new HealBulletType(5.2f, 13){{
healPercent = 3f;
}};
{
shootEffect = Fx.shootHeal;
smokeEffect = Fx.hitLaser;
hitEffect = Fx.hitLaser;
despawnEffect = Fx.hitLaser;
collidesTeam = true;
}
@Override
public boolean collides(Bullet b, Tile tile){
return tile.getTeam() != b.getTeam() || tile.entity.healthf() < 1f;
}
@Override
public void draw(Bullet b){
Draw.color(Pal.heal);
Lines.stroke(2f);
Lines.lineAngleCenter(b.x, b.y, b.rot(), 7f);
Draw.color(Color.white);
Lines.lineAngleCenter(b.x, b.y, b.rot(), 3f);
Draw.reset();
}
@Override
public void hitTile(Bullet b, Tile tile){
super.hit(b);
tile = tile.link();
if(tile.entity != null && tile.getTeam() == b.getTeam() && !(tile.block() instanceof BuildBlock)){
Effects.effect(Fx.healBlockFull, Pal.heal, tile.drawx(), tile.drawy(), tile.block().size);
tile.entity.healBy(healPercent / 100f * tile.entity.maxHealth());
}
}
};
healBulletBig = new HealBulletType(5.2f, 15){{
healPercent = 5.5f;
}};
fireball = new BulletType(1f, 4){
{

View file

@ -11,7 +11,6 @@ import io.anuke.mindustry.entities.type.*;
import io.anuke.mindustry.game.*;
import io.anuke.mindustry.graphics.*;
import io.anuke.mindustry.type.*;
import io.anuke.mindustry.type.Item.*;
import static io.anuke.mindustry.Vars.tilesize;
@ -552,7 +551,7 @@ public class Fx implements ContentList{
float length = 20f * e.finpow();
float size = 7f * e.fout();
Draw.rect(((Item)e.data).icon(Icon.large), e.x + Angles.trnsx(e.rotation, length), e.y + Angles.trnsy(e.rotation, length), size, size);
Draw.rect(((Item)e.data).icon(Cicon.medium), e.x + Angles.trnsx(e.rotation, length), e.y + Angles.trnsy(e.rotation, length), size, size);
});

View file

@ -220,7 +220,7 @@ public class Mechs implements ContentList{
dart = new Mech("dart-ship", true){
{
drillPower = 1;
mineSpeed = 0.9f;
mineSpeed = 3f;
speed = 0.5f;
drag = 0.09f;
health = 200f;

View file

@ -13,6 +13,7 @@ public class TechTree implements ContentList{
@Override
public void load(){
TechNode.context = null;
all = new Array<>();
root = node(coreShard, () -> {
@ -302,19 +303,24 @@ public class TechTree implements ContentList{
});
}
private TechNode node(Block block, Runnable children){
ItemStack[] requirements = new ItemStack[block.buildRequirements.length];
private static TechNode node(Block block, Runnable children){
ItemStack[] requirements = new ItemStack[block.requirements.length];
for(int i = 0; i < requirements.length; i++){
requirements[i] = new ItemStack(block.buildRequirements[i].item, 30 + block.buildRequirements[i].amount * 6);
requirements[i] = new ItemStack(block.requirements[i].item, 30 + block.requirements[i].amount * 6);
}
return new TechNode(block, requirements, children);
}
private TechNode node(Block block){
private static TechNode node(Block block){
return node(block, () -> {});
}
public static void create(Block parent, Block block){
TechNode.context = all.find(t -> t.block == parent);
node(block, () -> {});
}
public static class TechNode{
static TechNode context;
@ -322,19 +328,22 @@ public class TechTree implements ContentList{
public final ItemStack[] requirements;
public final Array<TechNode> children = new Array<>();
TechNode(Block block, ItemStack[] requirements, Runnable children){
if(context != null){
context.children.add(this);
TechNode(TechNode ccontext, Block block, ItemStack[] requirements, Runnable children){
if(ccontext != null){
ccontext.children.add(this);
}
this.block = block;
this.requirements = requirements;
TechNode last = context;
context = this;
children.run();
context = last;
context = ccontext;
all.add(this);
}
TechNode(Block block, ItemStack[] requirements, Runnable children){
this(context, block, requirements, children);
}
}
}

View file

@ -17,8 +17,8 @@ public class UnitTypes implements ContentList{
@Override
public void load(){
draug = new UnitType("draug", Draug.class, Draug::new){{
isFlying = true;
draug = new UnitType("draug", Draug::new){{
flying = true;
drag = 0.01f;
speed = 0.3f;
maxVelocity = 1.2f;
@ -32,13 +32,13 @@ public class UnitTypes implements ContentList{
}};
}};
spirit = new UnitType("spirit", Spirit.class, Spirit::new){{
isFlying = true;
spirit = new UnitType("spirit", Spirit::new){{
flying = true;
drag = 0.01f;
speed = 0.4f;
speed = 0.42f;
maxVelocity = 1.6f;
range = 50f;
health = 60;
health = 100;
engineSize = 1.8f;
engineOffset = 5.7f;
weapon = new Weapon("heal-blaster"){{
@ -48,22 +48,21 @@ public class UnitTypes implements ContentList{
roundrobin = true;
ejectEffect = Fx.none;
recoil = 2f;
bullet = Bullets.healBullet;
bullet = Bullets.healBulletBig;
shootSound = Sounds.pew;
}};
}};
phantom = new UnitType("phantom", Phantom.class, Phantom::new){{
isFlying = true;
phantom = new UnitType("phantom", Phantom::new){{
flying = true;
drag = 0.01f;
mass = 2f;
speed = 0.45f;
maxVelocity = 1.9f;
range = 70f;
itemCapacity = 70;
health = 220;
buildPower = 0.9f;
minePower = 1.1f;
health = 400;
buildPower = 1f;
engineOffset = 6.5f;
toMine = ObjectSet.with(Items.lead, Items.copper, Items.titanium);
weapon = new Weapon("heal-blaster"){{
@ -77,7 +76,7 @@ public class UnitTypes implements ContentList{
}};
}};
dagger = new UnitType("dagger", Dagger.class, Dagger::new){{
dagger = new UnitType("dagger", Dagger::new){{
maxVelocity = 1.1f;
speed = 0.2f;
drag = 0.4f;
@ -93,7 +92,7 @@ public class UnitTypes implements ContentList{
}};
}};
crawler = new UnitType("crawler", Crawler.class, Crawler::new){{
crawler = new UnitType("crawler", Crawler::new){{
maxVelocity = 1.27f;
speed = 0.285f;
drag = 0.4f;
@ -124,7 +123,7 @@ public class UnitTypes implements ContentList{
}};
}};
titan = new UnitType("titan", Titan.class, Titan::new){{
titan = new UnitType("titan", Titan::new){{
maxVelocity = 0.8f;
speed = 0.22f;
drag = 0.4f;
@ -146,7 +145,7 @@ public class UnitTypes implements ContentList{
}};
}};
fortress = new UnitType("fortress", Fortress.class, Fortress::new){{
fortress = new UnitType("fortress", Fortress::new){{
maxVelocity = 0.78f;
speed = 0.15f;
drag = 0.4f;
@ -168,7 +167,7 @@ public class UnitTypes implements ContentList{
}};
}};
eruptor = new UnitType("eruptor", Eruptor.class, Eruptor::new){{
eruptor = new UnitType("eruptor", Eruptor::new){{
maxVelocity = 0.81f;
speed = 0.16f;
drag = 0.4f;
@ -190,7 +189,7 @@ public class UnitTypes implements ContentList{
}};
}};
chaosArray = new UnitType("chaos-array", Dagger.class, Dagger::new){{
chaosArray = new UnitType("chaos-array", Dagger::new){{
maxVelocity = 0.68f;
speed = 0.12f;
drag = 0.4f;
@ -214,7 +213,7 @@ public class UnitTypes implements ContentList{
}};
}};
eradicator = new UnitType("eradicator", Dagger.class, Dagger::new){{
eradicator = new UnitType("eradicator", Dagger::new){{
maxVelocity = 0.68f;
speed = 0.12f;
drag = 0.4f;
@ -239,12 +238,12 @@ public class UnitTypes implements ContentList{
}};
}};
wraith = new UnitType("wraith", Wraith.class, Wraith::new){{
wraith = new UnitType("wraith", Wraith::new){{
speed = 0.3f;
maxVelocity = 1.9f;
drag = 0.01f;
mass = 1.5f;
isFlying = true;
flying = true;
health = 75;
engineOffset = 5.5f;
range = 140f;
@ -258,13 +257,13 @@ public class UnitTypes implements ContentList{
}};
}};
ghoul = new UnitType("ghoul", Ghoul.class, Ghoul::new){{
ghoul = new UnitType("ghoul", Ghoul::new){{
health = 220;
speed = 0.2f;
maxVelocity = 1.4f;
mass = 3f;
drag = 0.01f;
isFlying = true;
flying = true;
targetAir = false;
engineOffset = 7.8f;
range = 140f;
@ -282,7 +281,7 @@ public class UnitTypes implements ContentList{
}};
}};
revenant = new UnitType("revenant", Revenant.class, Revenant::new){{
revenant = new UnitType("revenant", Revenant::new){{
health = 1000;
mass = 5f;
hitsize = 20f;
@ -291,7 +290,7 @@ public class UnitTypes implements ContentList{
drag = 0.01f;
range = 80f;
shootCone = 40f;
isFlying = true;
flying = true;
rotateWeapon = true;
engineOffset = 12f;
engineSize = 3f;
@ -313,7 +312,7 @@ public class UnitTypes implements ContentList{
}};
}};
lich = new UnitType("lich", Revenant.class, Revenant::new){{
lich = new UnitType("lich", Revenant::new){{
health = 6000;
mass = 20f;
hitsize = 40f;
@ -322,7 +321,7 @@ public class UnitTypes implements ContentList{
drag = 0.02f;
range = 80f;
shootCone = 20f;
isFlying = true;
flying = true;
rotateWeapon = true;
engineOffset = 21;
engineSize = 5.3f;
@ -346,7 +345,7 @@ public class UnitTypes implements ContentList{
}};
}};
reaper = new UnitType("reaper", Revenant.class, Revenant::new){{
reaper = new UnitType("reaper", Revenant::new){{
health = 11000;
mass = 30f;
hitsize = 56f;
@ -355,7 +354,7 @@ public class UnitTypes implements ContentList{
drag = 0.02f;
range = 80f;
shootCone = 30f;
isFlying = true;
flying = true;
rotateWeapon = true;
engineOffset = 40;
engineSize = 7.3f;

View file

@ -11,6 +11,7 @@ import io.anuke.mindustry.type.*;
import io.anuke.mindustry.world.*;
import static io.anuke.arc.Core.files;
import static io.anuke.mindustry.Vars.mods;
/**
* Loads all game content.
@ -41,6 +42,14 @@ public class ContentLoader{
new LegacyColorMapper(),
};
/** Clears all initialized content.*/
public void clear(){
contentNameMap = new ObjectMap[ContentType.values().length];
contentMap = new Array[ContentType.values().length];
initialization = new ObjectSet<>();
loaded = false;
}
/** Creates all content types. */
public void createContent(){
if(loaded){
@ -57,20 +66,11 @@ public class ContentLoader{
list.load();
}
for(ContentType type : ContentType.values()){
for(Content c : contentMap[type.ordinal()]){
if(c instanceof MappableContent){
String name = ((MappableContent)c).name;
if(contentNameMap[type.ordinal()].containsKey(name)){
throw new IllegalArgumentException("Two content objects cannot have the same name! (issue: '" + name + "')");
}
contentNameMap[type.ordinal()].put(name, (MappableContent)c);
}
}
if(mods != null){
mods.loadContent();
}
//set up ID mapping
//check up ID mapping, make sure it's linear
for(Array<Content> arr : contentMap){
for(int i = 0; i < arr.size; i++){
int id = arr.get(i).id;
@ -109,6 +109,7 @@ public class ContentLoader{
for(ContentType type : ContentType.values()){
for(Content content : contentMap[type.ordinal()]){
//TODO catch error and display it per mod
callable.accept(content);
}
}
@ -138,6 +139,14 @@ public class ContentLoader{
public void handleContent(Content content){
contentMap[content.getContentType().ordinal()].add(content);
}
public void handleMappableContent(MappableContent content){
if(contentNameMap[content.getContentType().ordinal()].containsKey(content.name)){
throw new IllegalArgumentException("Two content objects cannot have the same name! (issue: '" + content.name + "')");
}
contentNameMap[content.getContentType().ordinal()].put(content.name, content);
}
public void setTemporaryMapper(MappableContent[][] temporaryMapper){

View file

@ -91,6 +91,7 @@ public class Control implements ApplicationListener, Loadable{
hiscore = true;
world.getMap().setHighScore(state.wave);
}
Sounds.wave.play();
});
@ -388,7 +389,10 @@ public class Control implements ApplicationListener, Loadable{
saves.update();
//update and load any requested assets
assets.update();
try{
assets.update();
}catch(Exception ignored){
}
input.updateState();

View file

@ -0,0 +1,34 @@
package io.anuke.mindustry.core;
import io.anuke.arc.*;
import io.anuke.arc.assets.loaders.*;
import io.anuke.arc.collection.*;
import io.anuke.arc.files.*;
/** Handles files in a modded context. */
public class FileTree implements FileHandleResolver{
private ObjectMap<String, FileHandle> files = new ObjectMap<>();
public void addFile(String path, FileHandle f){
files.put(path, f);
}
/** Gets an asset file.*/
public FileHandle get(String path){
if(files.containsKey(path)){
return files.get(path);
}else{
return Core.files.internal(path);
}
}
/** Clears all mod files.*/
public void clear(){
files.clear();
}
@Override
public FileHandle resolve(String fileName){
return get(fileName);
}
}

View file

@ -16,6 +16,7 @@ import io.anuke.mindustry.type.*;
import io.anuke.mindustry.world.*;
import io.anuke.mindustry.world.blocks.*;
import io.anuke.mindustry.world.blocks.BuildBlock.*;
import io.anuke.mindustry.world.blocks.power.*;
import static io.anuke.mindustry.Vars.*;
@ -31,19 +32,24 @@ public class Logic implements ApplicationListener{
public Logic(){
Events.on(WaveEvent.class, event -> {
for(Player p : playerGroup.all()){
p.respawns = state.rules.respawns;
}
if(world.isZone()){
world.getZone().updateWave(state.wave);
}
for (Player p : playerGroup.all()) {
p.respawns = state.rules.respawns;
}
});
Events.on(BlockDestroyEvent.class, event -> {
//blocks that get broken are appended to the team's broken block queue
Tile tile = event.tile;
Block block = tile.block();
//skip null entities or nukes, for obvious reasons
if(tile.entity == null || tile.block() instanceof NuclearReactor) return;
if(block instanceof BuildBlock){
BuildEntity entity = tile.entity();
//update block to reflect the fact that something was being constructed
@ -56,7 +62,33 @@ public class Logic implements ApplicationListener{
}
TeamData data = state.teams.get(tile.getTeam());
data.brokenBlocks.addFirst(BrokenBlock.get(tile.x, tile.y, tile.rotation(), block.id));
//remove existing blocks that have been placed here.
//painful O(n) iteration + copy
for(int i = 0; i < data.brokenBlocks.size; i++){
BrokenBlock b = data.brokenBlocks.get(i);
if(b.x == tile.x && b.y == tile.y){
data.brokenBlocks.removeIndex(i);
break;
}
}
data.brokenBlocks.addFirst(new BrokenBlock(tile.x, tile.y, tile.rotation(), block.id, tile.entity.config()));
});
Events.on(BlockBuildEndEvent.class, event -> {
if(!event.breaking){
TeamData data = state.teams.get(event.team);
//painful O(n) iteration + copy
for(int i = 0; i < data.brokenBlocks.size; i++){
BrokenBlock b = data.brokenBlocks.get(i);
if(b.x == event.tile.x && b.y == event.tile.y){
data.brokenBlocks.removeIndex(i);
break;
}
}
}
});
}

View file

@ -76,6 +76,7 @@ public class NetClient implements ApplicationListener{
ConnectPacket c = new ConnectPacket();
c.name = player.name;
c.mods = mods.getModStrings();
c.mobile = mobile;
c.versionType = Version.type;
c.color = Color.rgba8888(player.color);
@ -235,7 +236,7 @@ public class NetClient implements ApplicationListener{
netClient.disconnectQuietly();
state.set(State.menu);
logic.reset();
ui.showText("$disconnect", reason);
ui.showText("$disconnect", reason, Align.left);
ui.loadfrag.hide();
}
@ -329,6 +330,11 @@ public class NetClient implements ApplicationListener{
@Remote(variants = Variant.one, priority = PacketPriority.low, unreliable = true)
public static void onStateSnapshot(float waveTime, int wave, int enemies, short coreDataLen, byte[] coreData){
try{
if(wave > state.wave){
state.wave = wave;
Events.fire(new WaveEvent());
}
state.wavetime = waveTime;
state.wave = wave;
state.enemies = enemies;

View file

@ -104,6 +104,23 @@ public class NetServer implements ApplicationListener{
return;
}
Array<String> extraMods = packet.mods.copy();
Array<String> missingMods = mods.getIncompatibility(extraMods);
if(!extraMods.isEmpty() || !missingMods.isEmpty()){
//can't easily be localized since kick reasons can't have formatted text with them
StringBuilder result = new StringBuilder("[accent]Incompatible mods![]\n\n");
if(!missingMods.isEmpty()){
result.append("Missing:[lightgray]\n").append("> ").append(missingMods.toString("\n> "));
result.append("[]\n");
}
if(!extraMods.isEmpty()){
result.append("Unnecessary mods:[lightgray]\n").append("> ").append(extraMods.toString("\n> "));
}
con.kick(result.toString());
}
if(!admins.isWhitelisted(packet.uuid, packet.usid)){
info.adminUsid = packet.usid;
info.lastName = packet.name;
@ -200,6 +217,11 @@ public class NetServer implements ApplicationListener{
registerCommands();
}
@Override
public void init(){
mods.each(mod -> mod.registerClientCommands(clientCommands));
}
private void registerCommands(){
clientCommands.<Player>register("help", "[page]", "Lists all commands.", (args, player) -> {
if(args.length > 0 && !Strings.canParseInt(args[0])){
@ -262,7 +284,7 @@ public class NetServer implements ApplicationListener{
}
boolean checkPass(){
if(votes >= votesRequired() && target.isAdded() && target.con.isConnected()){
if(votes >= votesRequired()){
Call.sendMessage(Strings.format("[orange]Vote passed.[scarlet] {0}[orange] will be banned from the server for {1} minutes.", target.name, (kickDuration/60)));
target.getInfo().lastKicked = Time.millis() + kickDuration*1000;
playerGroup.all().each(p -> p.uuid != null && p.uuid.equals(target.uuid), p -> p.con.kick(KickReason.vote));

View file

@ -37,6 +37,10 @@ public interface Platform{
/** Steam: View a map listing on the workshop.*/
default void viewMapListing(String mapid){}
/** Steam: View map workshop info, removing the map ID tag if its listing is deleted.
* Also presents the option to update the map. */
default void viewMapListingInfo(Map map){}
/** Steam: Open workshop for maps.*/
default void openWorkshop(){}

View file

@ -18,12 +18,10 @@ import io.anuke.mindustry.entities.effect.*;
import io.anuke.mindustry.entities.effect.GroundEffectEntity.*;
import io.anuke.mindustry.entities.traits.*;
import io.anuke.mindustry.entities.type.*;
import io.anuke.mindustry.entities.type.EffectEntity;
import io.anuke.mindustry.game.EventType.*;
import io.anuke.mindustry.game.*;
import io.anuke.mindustry.game.EventType.*;
import io.anuke.mindustry.graphics.*;
import io.anuke.mindustry.input.*;
import io.anuke.mindustry.world.*;
import io.anuke.mindustry.world.blocks.defense.ForceProjector.*;
import static io.anuke.arc.Core.*;
@ -241,6 +239,8 @@ public class Renderer implements ApplicationListener{
blocks.drawBlocks(Layer.block);
blocks.drawFog();
blocks.drawBroken();
Draw.shader(Shaders.blockbuild, true);
blocks.drawBlocks(Layer.placement);
Draw.shader();
@ -311,7 +311,7 @@ public class Renderer implements ApplicationListener{
float fract = landTime / Fx.coreLand.lifetime;
TileEntity entity = player.getClosestCore();
TextureRegion reg = entity.block.icon(Block.Icon.full);
TextureRegion reg = entity.block.icon(Cicon.full);
float scl = Scl.scl(4f) / camerascale;
float s = reg.getWidth() * Draw.scl * scl * 4f * fract;

View file

@ -68,6 +68,7 @@ public class UI implements ApplicationListener, Loadable{
public DeployDialog deploy;
public TechTreeDialog tech;
public MinimapDialog minimap;
public ModsDialog mods;
public Cursor drillCursor, unloadCursor;
@ -108,6 +109,7 @@ public class UI implements ApplicationListener, Loadable{
ClickListener.clicked = () -> Sounds.press.play();
Colors.put("accent", Pal.accent);
Colors.put("unlaunched", Color.valueOf("8982ed"));
Colors.put("highlight", Pal.accent.cpy().lerp(Color.white, 0.3f));
Colors.put("stat", Pal.stat);
loadExtraCursors();
@ -222,6 +224,7 @@ public class UI implements ApplicationListener, Loadable{
deploy = new DeployDialog();
tech = new TechTreeDialog();
minimap = new MinimapDialog();
mods = new ModsDialog();
Group group = Core.scene.root;
@ -281,7 +284,7 @@ public class UI implements ApplicationListener, Loadable{
new Dialog(titleText){{
cont.margin(30).add(dtext).padRight(6f);
TextFieldFilter filter = inumeric ? TextFieldFilter.digitsOnly : (f, c) -> true;
TextField field = cont.addField(def, t -> {}).size(170f, 50f).get();
TextField field = cont.addField(def, t -> {}).size(330f, 50f).get();
field.setFilter((f, c) -> field.getText().length() < textLength && filter.acceptChar(f, c));
buttons.defaults().size(120, 54).pad(4);
buttons.addButton("$ok", () -> {
@ -358,11 +361,15 @@ public class UI implements ApplicationListener, Loadable{
}
public void showText(String titleText, String text){
showText(titleText, text, Align.center);
}
public void showText(String titleText, String text, int align){
new Dialog(titleText){{
cont.row();
cont.addImage().width(400f).pad(2).colspan(2).height(4f).color(Pal.accent);
cont.row();
cont.add(text).width(400f).wrap().get().setAlignment(Align.center, Align.center);
cont.add(text).width(400f).wrap().get().setAlignment(align, align);
cont.row();
buttons.addButton("$ok", this::hide).size(90, 50).pad(4);
}}.show();
@ -410,6 +417,34 @@ public class UI implements ApplicationListener, Loadable{
dialog.show();
}
public void showCustomConfirm(String title, String text, String yes, String no, Runnable confirmed){
FloatingDialog dialog = new FloatingDialog(title);
dialog.cont.add(text).width(500f).wrap().pad(4f).get().setAlignment(Align.center, Align.center);
dialog.buttons.defaults().size(200f, 54f).pad(2f);
dialog.setFillParent(false);
dialog.buttons.addButton(no, dialog::hide);
dialog.buttons.addButton(yes, () -> {
dialog.hide();
confirmed.run();
});
dialog.keyDown(KeyCode.ESCAPE, dialog::hide);
dialog.keyDown(KeyCode.BACK, dialog::hide);
dialog.show();
}
public void showOkText(String title, String text, Runnable confirmed){
FloatingDialog dialog = new FloatingDialog(title);
dialog.cont.add(text).width(500f).wrap().pad(4f).get().setAlignment(Align.center, Align.center);
dialog.buttons.defaults().size(200f, 54f).pad(2f);
dialog.setFillParent(false);
dialog.buttons.addButton("$ok", () -> {
dialog.hide();
confirmed.run();
});
dialog.show();
}
public String formatAmount(int number){
if(number >= 1000000){
return Strings.fixed(number / 1000000f, 1) + "[gray]mil[]";

View file

@ -1,11 +1,11 @@
package io.anuke.mindustry.core;
import io.anuke.annotations.Annotations.*;
import io.anuke.arc.*;
import io.anuke.arc.collection.*;
import io.anuke.arc.math.*;
import io.anuke.arc.math.geom.*;
import io.anuke.arc.util.*;
import io.anuke.arc.util.ArcAnnotate.*;
import io.anuke.mindustry.content.*;
import io.anuke.mindustry.core.GameState.*;
import io.anuke.mindustry.game.EventType.*;
@ -82,7 +82,8 @@ public class World{
return height()*tilesize;
}
public @Nullable Tile tile(int pos){
public @Nullable
Tile tile(int pos){
return tiles == null ? null : tile(Pos.x(pos), Pos.y(pos));
}

View file

@ -15,6 +15,7 @@ import io.anuke.arc.scene.style.*;
import io.anuke.arc.scene.ui.*;
import io.anuke.arc.scene.ui.layout.*;
import io.anuke.arc.util.*;
import io.anuke.arc.util.ArcAnnotate.*;
import io.anuke.mindustry.*;
import io.anuke.mindustry.content.*;
import io.anuke.mindustry.core.GameState.*;
@ -23,7 +24,7 @@ import io.anuke.mindustry.gen.*;
import io.anuke.mindustry.graphics.*;
import io.anuke.mindustry.io.*;
import io.anuke.mindustry.maps.*;
import io.anuke.mindustry.ui.Styles;
import io.anuke.mindustry.ui.*;
import io.anuke.mindustry.ui.dialogs.*;
import io.anuke.mindustry.world.*;
import io.anuke.mindustry.world.blocks.*;
@ -147,13 +148,19 @@ public class MapEditorDialog extends Dialog implements Disposable{
if(steam){
menu.cont.addImageTextButton("$editor.publish.workshop", Icon.linkSmall, () -> {
if(editor.getTags().containsKey("steamid")){
Map builtin = maps.all().find(m -> m.name().equals(editor.getTags().get("name", "").trim()));
if(editor.getTags().containsKey("steamid") && builtin != null && !builtin.custom){
platform.viewMapListing(editor.getTags().get("steamid"));
return;
}
Map map = save();
if(editor.getTags().containsKey("steamid") && map != null){
platform.viewMapListingInfo(map);
return;
}
if(map == null) return;
if(map.tags.get("description", "").length() < 4){
@ -167,7 +174,7 @@ public class MapEditorDialog extends Dialog implements Disposable{
}
platform.publishMap(map);
}).padTop(-3).size(swidth * 2f + 10, 60f).update(b -> b.setText(editor.getTags().containsKey("steamid") ? "$view.workshop" : "$editor.publish.workshop"));
}).padTop(-3).size(swidth * 2f + 10, 60f).update(b -> b.setText(editor.getTags().containsKey("steamid") ? editor.getTags().get("author").equals(player.name) ? "$workshop.listing" : "$view.workshop" : "$editor.publish.workshop"));
menu.cont.row();
}
@ -208,14 +215,6 @@ public class MapEditorDialog extends Dialog implements Disposable{
return;
}
Vector2 v = pane.stageToLocalCoordinates(Core.input.mouse());
if(v.x >= 0 && v.y >= 0 && v.x <= pane.getWidth() && v.y <= pane.getHeight()){
Core.scene.setScrollFocus(pane);
}else{
Core.scene.setScrollFocus(null);
}
if(Core.scene != null && Core.scene.getKeyboardFocus() == this){
doInput();
}
@ -287,7 +286,7 @@ public class MapEditorDialog extends Dialog implements Disposable{
});
}
public Map save(){
public @Nullable Map save(){
boolean isEditor = state.rules.editor;
state.rules.editor = false;
String name = editor.getTags().get("name", "").trim();
@ -681,6 +680,11 @@ public class MapEditorDialog extends Dialog implements Disposable{
pane = new ScrollPane(content);
pane.setFadeScrollBars(false);
pane.setOverscroll(true, false);
pane.exited(() -> {
if(pane.hasScroll()){
Core.scene.setScrollFocus(view);
}
});
ButtonGroup<ImageButton> group = new ButtonGroup<>();
int i = 0;
@ -698,7 +702,7 @@ public class MapEditorDialog extends Dialog implements Disposable{
});
for(Block block : blocksOut){
TextureRegion region = block.icon(Block.Icon.medium);
TextureRegion region = block.icon(Cicon.medium);
if(!Core.atlas.isFound(region)) continue;

View file

@ -388,7 +388,7 @@ public class MapGenerateDialog extends FloatingDialog{
GenTile tile = buffer1[px][py];
color = MapIO.colorFor(content.block(tile.floor), content.block(tile.block), content.block(tile.ore), Team.derelict);
}
pixmap.drawPixel(px, pixmap.getHeight() - 1 - py, color);
pixmap.draw(px, pixmap.getHeight() - 1 - py, color);
}
}

View file

@ -59,18 +59,25 @@ public class MapInfoDialog extends FloatingDialog{
t.row();
t.add("$editor.rules").padRight(8).left();
t.addButton("$edit", () -> ruleInfo.show(Vars.state.rules, () -> Vars.state.rules = new Rules())).left().width(200f);
t.addButton("$edit", () -> {
ruleInfo.show(Vars.state.rules, () -> Vars.state.rules = new Rules());
hide();
}).left().width(200f);
t.row();
t.add("$editor.waves").padRight(8).left();
t.addButton("$edit", waveInfo::show).left().width(200f);
t.addButton("$edit", () -> {
waveInfo.show();
hide();
}).left().width(200f);
t.row();
t.add("$editor.generation").padRight(8).left();
t.addButton("$edit",
() -> generate.show(Vars.maps.readFilters(editor.getTags().get("genfilters", "")),
filters -> editor.getTags().put("genfilters", JsonIO.write(filters)))
).left().width(200f);
t.addButton("$edit", () -> {
generate.show(Vars.maps.readFilters(editor.getTags().get("genfilters", "")),
filters -> editor.getTags().put("genfilters", JsonIO.write(filters)));
hide();
}).left().width(200f);
name.change();
description.change();

View file

@ -56,10 +56,16 @@ public class MapView extends Element implements GestureListener{
public boolean mouseMoved(InputEvent event, float x, float y){
mousex = x;
mousey = y;
requestScroll();
return false;
}
@Override
public void enter(InputEvent event, float x, float y, int pointer, Element fromActor){
requestScroll();
}
@Override
public boolean touchDown(InputEvent event, float x, float y, int pointer, KeyCode button){
if(pointer != 0){

View file

@ -140,7 +140,7 @@ public class WaveInfoDialog extends FloatingDialog{
t.margin(0).defaults().pad(3).padLeft(5f).growX().left();
t.addButton(b -> {
b.left();
b.addImage(group.type.iconRegion).size(30f).padRight(3);
b.addImage(group.type.icon(Cicon.medium)).size(32f).padRight(3);
b.add(group.type.localizedName).color(Pal.accent);
}, () -> showUpdate(group)).pad(-6f).padBottom(0f);
@ -221,7 +221,7 @@ public class WaveInfoDialog extends FloatingDialog{
for(UnitType type : content.units()){
dialog.cont.addButton(t -> {
t.left();
t.addImage(type.iconRegion).size(40f).padRight(2f);
t.addImage(type.icon(Cicon.medium)).size(40f).padRight(2f);
t.add(type.localizedName);
}, () -> {
lastType = type;
@ -253,7 +253,7 @@ public class WaveInfoDialog extends FloatingDialog{
for(int j = 0; j < spawned.length; j++){
if(spawned[j] > 0){
UnitType type = content.getByID(ContentType.unit, j);
table.addImage(type.iconRegion).size(30f).padRight(4);
table.addImage(type.icon(Cicon.medium)).size(8f * 4f).padRight(4);
table.add(spawned[j] + "x").color(Color.lightGray).padRight(6);
table.row();
}

View file

@ -20,6 +20,7 @@ public class Units{
private static float cdist;
private static boolean boolResult;
/** @return whether this player can interact with a specific tile. if either of these are null, returns true.*/
public static boolean canInteract(Player player, Tile tile){
return player == null || tile == null || tile.interactable(player.getTeam());
}

View file

@ -0,0 +1,50 @@
package io.anuke.mindustry.entities.bullet;
import io.anuke.arc.graphics.*;
import io.anuke.arc.graphics.g2d.*;
import io.anuke.mindustry.content.*;
import io.anuke.mindustry.entities.*;
import io.anuke.mindustry.entities.type.*;
import io.anuke.mindustry.graphics.*;
import io.anuke.mindustry.world.*;
import io.anuke.mindustry.world.blocks.*;
public class HealBulletType extends BulletType{
protected float healPercent = 3f;
public HealBulletType(float speed, float damage){
super(speed, damage);
shootEffect = Fx.shootHeal;
smokeEffect = Fx.hitLaser;
hitEffect = Fx.hitLaser;
despawnEffect = Fx.hitLaser;
collidesTeam = true;
}
@Override
public boolean collides(Bullet b, Tile tile){
return tile.getTeam() != b.getTeam() || tile.entity.healthf() < 1f;
}
@Override
public void draw(Bullet b){
Draw.color(Pal.heal);
Lines.stroke(2f);
Lines.lineAngleCenter(b.x, b.y, b.rot(), 7f);
Draw.color(Color.white);
Lines.lineAngleCenter(b.x, b.y, b.rot(), 3f);
Draw.reset();
}
@Override
public void hitTile(Bullet b, Tile tile){
super.hit(b);
tile = tile.link();
if(tile.entity != null && tile.getTeam() == b.getTeam() && !(tile.block() instanceof BuildBlock)){
Effects.effect(Fx.healBlockFull, Pal.heal, tile.drawx(), tile.drawy(), tile.block().size);
tile.entity.healBy(healPercent / 100f * tile.entity.maxHealth());
}
}
}

View file

@ -47,10 +47,8 @@ public class ItemTransfer extends TimedEntity implements DrawTrait{
@Remote(called = Loc.server)
public static void transferItemTo(Item item, int amount, float x, float y, Tile tile){
if(tile == null || tile.entity == null || tile.entity.items == null) return;
if(!Units.canInteract(player, tile)) return;
for(int i = 0; i < Mathf.clamp(amount / 3, 1, 8); i++){
Time.run(i * 3, () -> create(item, x, y, tile, () -> {
}));
Time.run(i * 3, () -> create(item, x, y, tile, () -> {}));
}
tile.entity.items.add(item, amount);
}

View file

@ -1,6 +1,5 @@
package io.anuke.mindustry.entities.traits;
import io.anuke.annotations.Annotations.*;
import io.anuke.arc.*;
import io.anuke.arc.collection.Queue;
import io.anuke.arc.collection.*;
@ -8,6 +7,7 @@ import io.anuke.arc.graphics.g2d.*;
import io.anuke.arc.math.*;
import io.anuke.arc.math.geom.*;
import io.anuke.arc.util.*;
import io.anuke.arc.util.ArcAnnotate.*;
import io.anuke.mindustry.*;
import io.anuke.mindustry.content.*;
import io.anuke.mindustry.entities.type.*;
@ -104,7 +104,11 @@ public interface BuilderTrait extends Entity, TeamTrait{
if(current.breaking){
entity.deconstruct(unit, core, 1f / entity.buildCost * Time.delta() * getBuildPower(tile) * state.rules.buildSpeedMultiplier);
}else{
entity.construct(unit, core, 1f / entity.buildCost * Time.delta() * getBuildPower(tile) * state.rules.buildSpeedMultiplier);
if(entity.construct(unit, core, 1f / entity.buildCost * Time.delta() * getBuildPower(tile) * state.rules.buildSpeedMultiplier)){
if(current.hasConfig){
Call.onTileConfig(null, tile, current.config);
}
}
}
current.progress = entity.progress;
@ -200,7 +204,8 @@ public interface BuilderTrait extends Entity, TeamTrait{
* Return the build requests currently active, or the one at the top of the queue.
* May return null.
*/
default @Nullable BuildRequest buildRequest(){
default @Nullable
BuildRequest buildRequest(){
return buildQueue().size == 0 ? null : buildQueue().first();
}
@ -256,6 +261,8 @@ public interface BuilderTrait extends Entity, TeamTrait{
public final int x, y, rotation;
public final Block block;
public final boolean breaking;
public boolean hasConfig;
public int config;
public float progress;
public boolean initialized;
@ -278,6 +285,12 @@ public interface BuilderTrait extends Entity, TeamTrait{
this.breaking = true;
}
public BuildRequest configure(int config){
this.config = config;
this.hasConfig = true;
return this;
}
public Tile tile(){
return world.tile(x, y);
}

View file

@ -6,6 +6,7 @@ import io.anuke.arc.graphics.g2d.*;
import io.anuke.arc.math.*;
import io.anuke.arc.math.geom.*;
import io.anuke.arc.util.*;
import io.anuke.arc.util.ArcAnnotate.*;
import io.anuke.mindustry.*;
import io.anuke.mindustry.content.*;
import io.anuke.mindustry.entities.*;
@ -93,7 +94,8 @@ public abstract class BaseUnit extends Unit implements ShooterTrait{
}
}
public @Nullable Tile getSpawner(){
public @Nullable
Tile getSpawner(){
return world.tile(spawner);
}
@ -234,7 +236,7 @@ public abstract class BaseUnit extends Unit implements ShooterTrait{
@Override
public TextureRegion getIconRegion(){
return type.iconRegion;
return type.icon(Cicon.full);
}
@Override
@ -263,7 +265,7 @@ public abstract class BaseUnit extends Unit implements ShooterTrait{
@Override
public boolean isFlying(){
return type.isFlying;
return type.flying;
}
@Override

View file

@ -10,6 +10,7 @@ import io.anuke.arc.math.geom.*;
import io.anuke.arc.scene.ui.*;
import io.anuke.arc.scene.ui.layout.*;
import io.anuke.arc.util.*;
import io.anuke.arc.util.ArcAnnotate.*;
import io.anuke.arc.util.pooling.*;
import io.anuke.mindustry.*;
import io.anuke.mindustry.content.*;
@ -48,7 +49,8 @@ public class Player extends Unit implements BuilderMinerTrait, ShooterTrait{
public float baseRotation;
public float pointerX, pointerY;
public String name = "noname";
public @Nullable String uuid, usid;
public @Nullable
String uuid, usid;
public boolean isAdmin, isTransferring, isShooting, isBoosting, isMobile, isTyping;
public float boostHeat, shootHeat, destructTime;
public boolean achievedFlight;
@ -158,7 +160,7 @@ public class Player extends Unit implements BuilderMinerTrait, ShooterTrait{
@Override
public TextureRegion getIconRegion(){
return mech.iconRegion;
return mech.icon(Cicon.full);
}
@Override
@ -279,7 +281,7 @@ public class Player extends Unit implements BuilderMinerTrait, ShooterTrait{
public void drawShadow(float offsetX, float offsetY){
float scl = mech.flying ? 1f : boostHeat / 2f;
Draw.rect(mech.iconRegion, x + offsetX * scl, y + offsetY * scl, rotation - 90);
Draw.rect(getIconRegion(), x + offsetX * scl, y + offsetY * scl, rotation - 90);
}
@Override

View file

@ -7,6 +7,7 @@ import io.anuke.arc.collection.ObjectSet;
import io.anuke.arc.math.geom.Point2;
import io.anuke.arc.math.geom.Vector2;
import io.anuke.arc.util.*;
import io.anuke.arc.util.ArcAnnotate.*;
import io.anuke.mindustry.entities.EntityGroup;
import io.anuke.mindustry.entities.traits.HealthTrait;
import io.anuke.mindustry.entities.traits.TargetTrait;
@ -230,6 +231,11 @@ public class TileEntity extends BaseEntity implements TargetTrait, HealthTrait{
return proximity;
}
/** Tile configuration. Defaults to 0. Used for block rebuilding. */
public int config(){
return 0;
}
@Override
public void removed(){
if(sound != null){

View file

@ -1,6 +1,5 @@
package io.anuke.mindustry.entities.type;
import io.anuke.annotations.Annotations.*;
import io.anuke.arc.*;
import io.anuke.arc.collection.*;
import io.anuke.arc.graphics.*;
@ -9,6 +8,7 @@ import io.anuke.arc.math.*;
import io.anuke.arc.math.geom.*;
import io.anuke.arc.scene.ui.layout.*;
import io.anuke.arc.util.*;
import io.anuke.arc.util.ArcAnnotate.*;
import io.anuke.mindustry.content.*;
import io.anuke.mindustry.entities.*;
import io.anuke.mindustry.entities.effect.*;
@ -216,10 +216,14 @@ public abstract class Unit extends DestructibleEntity implements SaveTrait, Targ
float cx = x - fsize/2f, cy = y - fsize/2f;
for(Team team : Team.all){
avoid(unitGroups[team.ordinal()].intersect(cx, cy, fsize, fsize));
if(team != getTeam() || !(this instanceof Player)){
avoid(unitGroups[team.ordinal()].intersect(cx, cy, fsize, fsize));
}
}
avoid(playerGroup.intersect(cx, cy, fsize, fsize));
if(!(this instanceof Player)){
avoid(playerGroup.intersect(cx, cy, fsize, fsize));
}
velocity.add(moveVector.x / mass() * Time.delta(), moveVector.y / mass() * Time.delta());
}
@ -227,7 +231,7 @@ public abstract class Unit extends DestructibleEntity implements SaveTrait, Targ
float radScl = 1.5f;
for(Unit en : arr){
if(en.isFlying() != isFlying()) continue;
if(en.isFlying() != isFlying() || (en instanceof Player && en.getTeam() != getTeam())) continue;
float dst = dst(en);
float scl = Mathf.clamp(1f - dst / (getSize()/(radScl*2f) + en.getSize()/(radScl*2f)));
moveVector.add(Tmp.v1.set((x - en.x) * scl, (y - en.y) * scl).limit(0.4f));
@ -403,7 +407,7 @@ public abstract class Unit extends DestructibleEntity implements SaveTrait, Targ
float size = (itemSize + Mathf.absin(Time.time(), 5f, 1f)) * itemtime;
Draw.mixcol(Pal.accent, Mathf.absin(Time.time(), 5f, 0.5f));
Draw.rect(item.item.icon(Item.Icon.large),
Draw.rect(item.item.icon(Cicon.medium),
x + Angles.trnsx(rotation + 180f, backTrns),
y + Angles.trnsy(rotation + 180f, backTrns),
size, size, rotation);

View file

@ -2,7 +2,6 @@ package io.anuke.mindustry.entities.type.base;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.math.geom.Geometry;
import io.anuke.mindustry.entities.type.FlyingUnit;
import io.anuke.mindustry.entities.units.*;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.meta.BlockFlag;

View file

@ -1,23 +1,19 @@
package io.anuke.mindustry.entities.type.base;
import io.anuke.arc.Core;
import io.anuke.arc.Events;
import io.anuke.arc.collection.IntIntMap;
import io.anuke.arc.collection.Queue;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.*;
import io.anuke.arc.collection.*;
import io.anuke.arc.math.*;
import io.anuke.arc.util.*;
import io.anuke.mindustry.Vars;
import io.anuke.mindustry.entities.EntityGroup;
import io.anuke.mindustry.entities.traits.BuilderTrait;
import io.anuke.mindustry.entities.traits.TargetTrait;
import io.anuke.mindustry.*;
import io.anuke.mindustry.entities.*;
import io.anuke.mindustry.entities.traits.*;
import io.anuke.mindustry.entities.type.*;
import io.anuke.mindustry.entities.units.UnitState;
import io.anuke.mindustry.game.EventType.BuildSelectEvent;
import io.anuke.mindustry.game.Teams.TeamData;
import io.anuke.mindustry.gen.BrokenBlock;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.world.blocks.BuildBlock;
import io.anuke.mindustry.world.blocks.BuildBlock.BuildEntity;
import io.anuke.mindustry.entities.units.*;
import io.anuke.mindustry.game.EventType.*;
import io.anuke.mindustry.game.Teams.*;
import io.anuke.mindustry.world.*;
import io.anuke.mindustry.world.blocks.*;
import io.anuke.mindustry.world.blocks.BuildBlock.*;
import java.io.*;
@ -45,7 +41,7 @@ public class BuilderDrone extends BaseDrone implements BuilderTrait{
BuildEntity entity = (BuildEntity)target;
TileEntity core = getClosestCore();
if(isBuilding() && entity == null && isRebuild()){
if(isBuilding() && entity == null && canRebuild()){
target = world.tile(buildRequest().x, buildRequest().y);
circle(placeDistance * 0.7f);
target = null;
@ -100,9 +96,9 @@ public class BuilderDrone extends BaseDrone implements BuilderTrait{
incDrones(playerTarget);
TargetTrait prev = target;
target = playerTarget;
float dst = 90f + (id % 4)*30;
float dst = 90f + (id % 10)*3;
float tdst = dst(target);
float scale = (Mathf.lerp(1f, 0.77f, 1f - Mathf.clamp((tdst - dst) / dst)));
float scale = (Mathf.lerp(1f, 0.2f, 1f - Mathf.clamp((tdst - dst) / dst)));
circle(dst);
velocity.scl(scale);
target = prev;
@ -151,9 +147,8 @@ public class BuilderDrone extends BaseDrone implements BuilderTrait{
}
}
boolean isRebuild(){
//disabled until further notice, reason being that it's too annoying when playing enemies and too broken for ally use
return false; //Vars.state.rules.enemyCheat && team == waveTeam;
boolean canRebuild(){
return true;
}
@Override
@ -188,13 +183,14 @@ public class BuilderDrone extends BaseDrone implements BuilderTrait{
}
}
if(isRebuild() && !isBuilding()){
if(timer.get(timerTarget, 80) && Units.closestEnemy(getTeam(), x, y, 100f, u -> !(u instanceof BaseDrone)) == null && !isBuilding()){
TeamData data = Vars.state.teams.get(team);
if(!data.brokenBlocks.isEmpty()){
long block = data.brokenBlocks.removeLast();
placeQueue.addFirst(new BuildRequest(BrokenBlock.x(block), BrokenBlock.y(block), BrokenBlock.rotation(block), content.block(BrokenBlock.block(block))));
setState(build);
BrokenBlock block = data.brokenBlocks.removeLast();
if(Build.validPlace(getTeam(), block.x, block.y, content.block(block.block), block.rotation)){
placeQueue.addFirst(new BuildRequest(block.x, block.y, block.rotation, content.block(block.block)).configure(block.config));
setState(build);
}
}
}
}

View file

@ -1,6 +1,4 @@
package io.anuke.mindustry.entities.type.base;
import io.anuke.mindustry.entities.type.GroundUnit;
public class Crawler extends GroundUnit{
}

View file

@ -1,7 +1,5 @@
package io.anuke.mindustry.entities.type.base;
import io.anuke.mindustry.entities.type.GroundUnit;
public class Dagger extends GroundUnit{
}

View file

@ -1,6 +1,4 @@
package io.anuke.mindustry.entities.type.base;
import io.anuke.mindustry.entities.type.GroundUnit;
public class Eruptor extends GroundUnit{
}

View file

@ -1,4 +1,4 @@
package io.anuke.mindustry.entities.type;
package io.anuke.mindustry.entities.type.base;
import io.anuke.arc.graphics.*;
import io.anuke.arc.graphics.g2d.*;
@ -8,6 +8,7 @@ import io.anuke.arc.util.*;
import io.anuke.mindustry.*;
import io.anuke.mindustry.entities.*;
import io.anuke.mindustry.entities.bullet.*;
import io.anuke.mindustry.entities.type.*;
import io.anuke.mindustry.entities.units.*;
import io.anuke.mindustry.graphics.*;
import io.anuke.mindustry.world.*;
@ -15,7 +16,7 @@ import io.anuke.mindustry.world.meta.*;
import static io.anuke.mindustry.Vars.*;
public abstract class FlyingUnit extends BaseUnit{
public class FlyingUnit extends BaseUnit{
protected float[] weaponAngles = {0,0};
protected final UnitState
@ -36,6 +37,10 @@ public abstract class FlyingUnit extends BaseUnit{
if(target == null) targetClosestEnemyFlag(BlockFlag.producer);
if(target == null) targetClosestEnemyFlag(BlockFlag.turret);
if(target == null && isCommanded() && getCommand() != UnitCommand.attack){
onCommand(getCommand());
}
}
if(getClosestSpawner() == null && getSpawner() != null && target == null){

View file

@ -1,6 +1,4 @@
package io.anuke.mindustry.entities.type.base;
import io.anuke.mindustry.entities.type.GroundUnit;
public class Fortress extends GroundUnit{
}

View file

@ -1,7 +1,5 @@
package io.anuke.mindustry.entities.type.base;
import io.anuke.mindustry.entities.type.FlyingUnit;
public class Ghoul extends FlyingUnit{
}

View file

@ -1,4 +1,4 @@
package io.anuke.mindustry.entities.type;
package io.anuke.mindustry.entities.type.base;
import io.anuke.arc.graphics.*;
import io.anuke.arc.graphics.g2d.*;
@ -9,6 +9,7 @@ import io.anuke.mindustry.*;
import io.anuke.mindustry.ai.Pathfinder.*;
import io.anuke.mindustry.entities.*;
import io.anuke.mindustry.entities.bullet.*;
import io.anuke.mindustry.entities.type.*;
import io.anuke.mindustry.entities.units.*;
import io.anuke.mindustry.game.*;
import io.anuke.mindustry.type.*;
@ -18,7 +19,7 @@ import io.anuke.mindustry.world.meta.*;
import static io.anuke.mindustry.Vars.*;
public abstract class GroundUnit extends BaseUnit{
public class GroundUnit extends BaseUnit{
protected static Vector2 vec = new Vector2();
protected float walkTime;

View file

@ -4,7 +4,6 @@ import io.anuke.arc.graphics.g2d.Draw;
import io.anuke.arc.math.Angles;
import io.anuke.arc.math.Mathf;
import io.anuke.mindustry.entities.Units;
import io.anuke.mindustry.entities.type.FlyingUnit;
public class Revenant extends FlyingUnit{

View file

@ -1,7 +1,5 @@
package io.anuke.mindustry.entities.type.base;
import io.anuke.mindustry.entities.type.GroundUnit;
public class Titan extends GroundUnit{
}

View file

@ -1,7 +1,5 @@
package io.anuke.mindustry.entities.type.base;
import io.anuke.mindustry.entities.type.FlyingUnit;
public class Wraith extends FlyingUnit{
}

View file

@ -0,0 +1,23 @@
package io.anuke.mindustry.game;
import java.util.*;
/** Defines sizes of a content's preview icon. */
public enum Cicon{
/** Full size. */
full(0),
tiny(8 * 2),
small(8 * 3),
medium(8 * 4),
large(8 * 5),
xlarge(8 * 6);
public static final Cicon[] all = values();
public static final Cicon[] scaled = Arrays.copyOfRange(all, 1, all.length);
public final int size;
Cicon(int size){
this.size = size;
}
}

View file

@ -1,12 +1,16 @@
package io.anuke.mindustry.game;
import io.anuke.arc.util.ArcAnnotate.*;
import io.anuke.mindustry.Vars;
import io.anuke.mindustry.mod.Mods.*;
import io.anuke.mindustry.type.ContentType;
/** Base class for a content type that is loaded in {@link io.anuke.mindustry.core.ContentLoader}. */
public abstract class Content{
public final short id;
/** The mod that loaded this piece of content. */
public @Nullable LoadedMod mod;
public Content(){
this.id = (short)Vars.content.getBy(getContentType()).size;

View file

@ -1,6 +1,6 @@
package io.anuke.mindustry.game;
import io.anuke.annotations.Annotations.*;
import io.anuke.arc.util.ArcAnnotate.*;
import io.anuke.mindustry.core.GameState.State;
import io.anuke.mindustry.entities.traits.BuilderTrait;
import io.anuke.mindustry.entities.type.*;
@ -83,6 +83,10 @@ public class EventType{
}
public static class ContentReloadEvent{
}
public static class DisposeEvent{
}
@ -195,7 +199,8 @@ public class EventType{
public static class BlockBuildEndEvent{
public final Tile tile;
public final Team team;
public final @Nullable Player player;
public final @Nullable
Player player;
public final boolean breaking;
public BlockBuildEndEvent(Tile tile, @Nullable Player player, Team team, boolean breaking){

View file

@ -150,6 +150,7 @@ public class GlobalData{
@SuppressWarnings("unchecked")
public void load(){
items.clear();
unlocked = Core.settings.getObject("unlocks", ObjectMap.class, ObjectMap::new);
for(Item item : Vars.content.items()){
items.put(item, Core.settings.getInt("item-" + item.name, 0));

View file

@ -1,10 +1,13 @@
package io.anuke.mindustry.game;
import io.anuke.mindustry.*;
public abstract class MappableContent extends Content{
public final String name;
public MappableContent(String name){
this.name = name;
Vars.content.handleMappableContent(this);
}
@Override

View file

@ -1,11 +1,11 @@
package io.anuke.mindustry.game;
import io.anuke.annotations.Annotations.*;
import io.anuke.arc.*;
import io.anuke.arc.audio.*;
import io.anuke.arc.collection.*;
import io.anuke.arc.math.*;
import io.anuke.arc.util.*;
import io.anuke.arc.util.ArcAnnotate.*;
import io.anuke.mindustry.core.GameState.*;
import io.anuke.mindustry.game.EventType.*;
import io.anuke.mindustry.gen.*;
@ -22,7 +22,8 @@ public class MusicControl{
public Array<Music> darkMusic = Array.with();
private Music lastRandomPlayed;
private Interval timer = new Interval();
private @Nullable Music current;
private @Nullable
Music current;
private float fade;
private boolean silenced;

View file

@ -7,6 +7,7 @@ import io.anuke.arc.files.*;
import io.anuke.arc.graphics.*;
import io.anuke.arc.util.*;
import io.anuke.arc.util.async.*;
import io.anuke.mindustry.*;
import io.anuke.mindustry.core.GameState.*;
import io.anuke.mindustry.game.EventType.*;
import io.anuke.mindustry.io.*;
@ -253,6 +254,17 @@ public class Saves{
return meta.map;
}
public void cautiousLoad(Runnable run){
Array<String> mods = Array.with(getMods());
mods.removeAll(Vars.mods.getModStrings());
if(!mods.isEmpty()){
ui.showConfirm("$warning", Core.bundle.format("mod.missing", mods.toString("\n")), run);
}else{
run.run();
}
}
public String getName(){
return Core.settings.getString("save-" + index + "-name", "untitled");
}
@ -262,6 +274,10 @@ public class Saves{
Core.settings.save();
}
public String[] getMods(){
return meta.mods;
}
public Zone getZone(){
return meta == null || meta.rules == null ? null : meta.rules.zone;
}

View file

@ -1,9 +1,8 @@
package io.anuke.mindustry.game;
import io.anuke.annotations.Annotations.Struct;
import io.anuke.arc.collection.*;
import io.anuke.mindustry.Vars;
import io.anuke.mindustry.world.Tile;
import io.anuke.mindustry.*;
import io.anuke.mindustry.world.*;
/** Class for various team-based utilities. */
public class Teams{
@ -52,7 +51,7 @@ public class Teams{
public final ObjectSet<Tile> cores = new ObjectSet<>();
public final EnumSet<Team> enemies;
public final Team team;
public LongQueue brokenBlocks = new LongQueue();
public Queue<BrokenBlock> brokenBlocks = new Queue<>();
public TeamData(Team team, EnumSet<Team> enemies){
this.team = team;
@ -62,8 +61,16 @@ public class Teams{
/** Represents a block made by this team that was destroyed somewhere on the map.
* This does not include deconstructed blocks.*/
@Struct
public class BrokenBlockStruct{
public short x, y, rotation, block;
public static class BrokenBlock{
public final short x, y, rotation, block;
public final int config;
public BrokenBlock(short x, short y, short rotation, short block, int config){
this.x = x;
this.y = y;
this.rotation = rotation;
this.block = block;
this.config = config;
}
}
}

View file

@ -1,9 +1,10 @@
package io.anuke.mindustry.game;
import io.anuke.arc.Core;
import io.anuke.arc.graphics.g2d.TextureRegion;
import io.anuke.arc.scene.ui.layout.Table;
import io.anuke.mindustry.Vars;
import io.anuke.annotations.Annotations.*;
import io.anuke.arc.*;
import io.anuke.arc.graphics.g2d.*;
import io.anuke.arc.scene.ui.layout.*;
import io.anuke.mindustry.*;
/** Base interface for an unlockable content type. */
public abstract class UnlockableContent extends MappableContent{
@ -11,6 +12,8 @@ public abstract class UnlockableContent extends MappableContent{
public String localizedName;
/** Localized description. May be null. */
public String description;
/** Icons by Cicon ID.*/
protected TextureRegion[] cicons = new TextureRegion[Cicon.all.length];
public UnlockableContent(String name){
super(name);
@ -19,10 +22,24 @@ public abstract class UnlockableContent extends MappableContent{
this.description = Core.bundle.getOrNull(getContentType() + "." + name + ".description");
}
/** Generate any special icons for this content. Called asynchronously.*/
@CallSuper
public void createIcons(PixmapPacker out, PixmapPacker editor){
}
/** Returns a specific content icon, or the region {contentType}-{name} if not found.*/
public TextureRegion icon(Cicon icon){
if(cicons[icon.ordinal()] == null){
cicons[icon.ordinal()] = Core.atlas.find(getContentType().name() + "-" + name + "-" + icon.name(), Core.atlas.find(getContentType().name() + "-" + name + "-full", Core.atlas.find(getContentType().name() + "-" + name, Core.atlas.find(name))));
}
return cicons[icon.ordinal()];
}
/** Returns the localized name of this content. */
public abstract String localizedName();
public abstract TextureRegion getContentIcon();
//public abstract TextureRegion getContentIcon();
/** This should show all necessary info about this content in the specified table. */
public abstract void displayInfo(Table table);

View file

@ -2,15 +2,19 @@ package io.anuke.mindustry.graphics;
import io.anuke.arc.*;
import io.anuke.arc.collection.*;
import io.anuke.arc.graphics.Color;
import io.anuke.arc.graphics.Texture.TextureFilter;
import io.anuke.arc.graphics.*;
import io.anuke.arc.graphics.Texture.*;
import io.anuke.arc.graphics.g2d.*;
import io.anuke.arc.graphics.glutils.FrameBuffer;
import io.anuke.arc.graphics.glutils.*;
import io.anuke.arc.math.*;
import io.anuke.arc.util.*;
import io.anuke.mindustry.content.Blocks;
import io.anuke.mindustry.content.*;
import io.anuke.mindustry.entities.type.base.*;
import io.anuke.mindustry.game.EventType.*;
import io.anuke.mindustry.game.Team;
import io.anuke.mindustry.game.*;
import io.anuke.mindustry.game.Teams.*;
import io.anuke.mindustry.world.*;
import io.anuke.mindustry.world.Block.*;
import static io.anuke.arc.Core.camera;
import static io.anuke.mindustry.Vars.*;
@ -120,6 +124,20 @@ public class BlockRenderer implements Disposable{
Draw.shader();
}
public void drawBroken(){
if(unitGroups[player.getTeam().ordinal()].all().contains(p -> p instanceof BuilderDrone)){
for(BrokenBlock block : state.teams.get(player.getTeam()).brokenBlocks){
Block b = content.block(block.block);
if(!camera.bounds(Tmp.r1).grow(tilesize * 2f).overlaps(Tmp.r2.setSize(b.size * tilesize).setCenter(block.x * tilesize + b.offset(), block.y * tilesize + b.offset()))) continue;
Draw.alpha(0.5f);
Draw.mixcol(Pal.accent, 0.2f + Mathf.absin(5f, 0.2f));
Draw.rect(b.icon(Cicon.full), block.x * tilesize + b.offset(), block.y * tilesize + b.offset(), b.rotate ? block.rotation * 90 : 0f);
}
Draw.reset();
}
}
public void drawShadows(){
if(!shadowEvents.isEmpty()){
Draw.flush();

View file

@ -14,6 +14,7 @@ import io.anuke.arc.util.noise.RidgedPerlin;
import io.anuke.arc.util.noise.Simplex;
import io.anuke.mindustry.content.Blocks;
import io.anuke.mindustry.content.UnitTypes;
import io.anuke.mindustry.game.*;
import io.anuke.mindustry.type.UnitType;
import io.anuke.mindustry.world.*;
import io.anuke.mindustry.world.blocks.Floor;
@ -252,7 +253,9 @@ public class MenuRenderer implements Disposable{
private void drawFlyers(){
Draw.color(0f, 0f, 0f, 0.4f);
float size = Math.max(flyerType.iconRegion.getWidth(), flyerType.iconRegion.getHeight()) * Draw.scl * 1.6f;
TextureRegion icon = flyerType.icon(Cicon.full);
float size = Math.max(icon.getWidth(), icon.getHeight()) * Draw.scl * 1.6f;
flyers((x, y) -> {
Draw.rect(flyerType.region, x - 12f, y - 13f, flyerRot - 90);

View file

@ -106,7 +106,7 @@ public class MinimapRenderer implements Disposable{
public void updateAll(){
for(int x = 0; x < world.width(); x++){
for(int y = 0; y < world.height(); y++){
pixmap.drawPixel(x, pixmap.getHeight() - 1 - y, colorFor(world.tile(x, y)));
pixmap.draw(x, pixmap.getHeight() - 1 - y, colorFor(world.tile(x, y)));
}
}
texture.draw(pixmap, 0, 0);
@ -114,7 +114,7 @@ public class MinimapRenderer implements Disposable{
public void update(Tile tile){
int color = colorFor(world.tile(tile.x, tile.y));
pixmap.drawPixel(tile.x, pixmap.getHeight() - 1 - tile.y, color);
pixmap.draw(tile.x, pixmap.getHeight() - 1 - tile.y, color);
Pixmaps.drawPixel(texture, tile.x, pixmap.getHeight() - 1 - tile.y, color);
}

View file

@ -1,22 +1,18 @@
package io.anuke.mindustry.graphics;
import io.anuke.arc.Core;
import io.anuke.arc.graphics.Color;
import io.anuke.arc.graphics.g2d.Draw;
import io.anuke.arc.graphics.g2d.Lines;
import io.anuke.arc.math.Mathf;
import io.anuke.arc.math.geom.Rectangle;
import io.anuke.arc.math.geom.Vector2;
import io.anuke.arc.util.Time;
import io.anuke.arc.util.Tmp;
import io.anuke.mindustry.Vars;
import io.anuke.mindustry.content.Blocks;
import io.anuke.mindustry.entities.Units;
import io.anuke.mindustry.entities.type.Player;
import io.anuke.mindustry.game.Team;
import io.anuke.mindustry.input.InputHandler;
import io.anuke.mindustry.type.Item;
import io.anuke.mindustry.world.Tile;
import io.anuke.arc.*;
import io.anuke.arc.graphics.*;
import io.anuke.arc.graphics.g2d.*;
import io.anuke.arc.math.*;
import io.anuke.arc.math.geom.*;
import io.anuke.arc.util.*;
import io.anuke.mindustry.*;
import io.anuke.mindustry.content.*;
import io.anuke.mindustry.entities.*;
import io.anuke.mindustry.entities.type.*;
import io.anuke.mindustry.game.*;
import io.anuke.mindustry.input.*;
import io.anuke.mindustry.world.*;
import static io.anuke.mindustry.Vars.*;
@ -112,6 +108,13 @@ public class OverlayRenderer{
if(tile != null && tile.block() != Blocks.air && tile.getTeam() == player.getTeam()){
tile.block().drawSelect(tile);
if(Core.input.keyDown(Binding.rotateplaced) && tile.block().rotate){
control.input.drawArrow(tile.block(), tile.x, tile.y, tile.rotation(), true);
Draw.color(Pal.accent, 0.3f + Mathf.absin(4f, 0.2f));
Fill.square(tile.drawx(), tile.drawy(), tile.block().size * tilesize/2f);
Draw.color();
}
}
}
@ -119,7 +122,7 @@ public class OverlayRenderer{
if(input.isDroppingItem()){
Vector2 v = Core.input.mouseWorld(input.getMouseX(), input.getMouseY());
float size = 8;
Draw.rect(player.item().item.icon(Item.Icon.large), v.x, v.y, size, size);
Draw.rect(player.item().item.icon(Cicon.medium), v.x, v.y, size, size);
Draw.color(Pal.accent);
Lines.circle(v.x, v.y, 6 + Mathf.absin(Time.time(), 5f, 1f));
Draw.reset();

View file

@ -1,17 +1,18 @@
package io.anuke.mindustry.graphics;
import io.anuke.annotations.Annotations.*;
import io.anuke.arc.Core;
import io.anuke.arc.graphics.Color;
import io.anuke.arc.graphics.g2d.TextureRegion;
import io.anuke.arc.graphics.glutils.Shader;
import io.anuke.arc.scene.ui.layout.Scl;
import io.anuke.arc.util.ArcAnnotate.*;
import io.anuke.arc.util.Time;
public class Shaders{
public static Shadow shadow;
public static BlockBuild blockbuild;
public static @Nullable Shield shield;
public static @Nullable
Shield shield;
public static UnitBuild build;
public static FogShader fog;
public static MenuShader menu;

View file

@ -13,6 +13,7 @@ public enum Binding implements KeyBind{
deselect(KeyCode.MOUSE_RIGHT),
break_block(KeyCode.MOUSE_RIGHT),
rotate(new Axis(KeyCode.SCROLL)),
rotateplaced(KeyCode.R),
diagonal_placement(KeyCode.CONTROL_LEFT),
pick(KeyCode.MOUSE_MIDDLE),
dash(KeyCode.SHIFT_LEFT),

View file

@ -11,9 +11,9 @@ import io.anuke.arc.util.*;
import io.anuke.mindustry.content.*;
import io.anuke.mindustry.core.GameState.*;
import io.anuke.mindustry.game.EventType.*;
import io.anuke.mindustry.gen.*;
import io.anuke.mindustry.graphics.*;
import io.anuke.mindustry.input.PlaceUtils.*;
import io.anuke.mindustry.net.Net;
import io.anuke.mindustry.world.*;
import static io.anuke.arc.Core.scene;
@ -189,6 +189,10 @@ public class DesktopInput extends InputHandler{
if(canTapPlayer(Core.input.mouseWorld().x, Core.input.mouseWorld().y)){
cursorType = ui.unloadCursor;
}
if(!isPlacing() && Math.abs(Core.input.axisTap(Binding.rotate)) > 0 && Core.input.keyDown(Binding.rotateplaced) && cursor.block().rotate){
Call.rotateBlock(player, cursor, Core.input.axisTap(Binding.rotate) > 0);
}
}
if(!Core.scene.hasMouse()){

View file

@ -57,6 +57,20 @@ public abstract class InputHandler implements InputProcessor{
player.clearItem();
}
@Remote(targets = Loc.both, called = Loc.server, forward = true, unreliable = true)
public static void rotateBlock(Player player, Tile tile, boolean direction){
if(net.server() && !Units.canInteract(player, tile)){
throw new ValidateException(player, "Player cannot drop an item.");
}
tile.rotation(Mathf.mod(tile.rotation() + Mathf.sign(direction), 4));
if(tile.entity != null){
tile.entity.updateProximity();
tile.entity.noSleep();
}
}
@Remote(targets = Loc.both, forward = true, called = Loc.server)
public static void transferInventory(Player player, Tile tile){
if(player == null || player.timer == null || !player.timer.get(Player.timerTransfer, 40)) return;
@ -114,6 +128,12 @@ public abstract class InputHandler implements InputProcessor{
tile.block().tapped(tile, player);
}
@Remote(targets = Loc.both, called = Loc.both, forward = true)
public static void onTileConfig(Player player, Tile tile, int value){
if(tile == null || !Units.canInteract(player, tile)) return;
tile.block().configured(tile, player, value);
}
public OverlayFragment getFrag(){
return frag;
}
@ -352,15 +372,19 @@ public abstract class InputHandler implements InputProcessor{
player.addBuildRequest(new BuildRequest(tile.x, tile.y));
}
void drawArrow(Block block, int x, int y, int rotation){
Draw.color(!validPlace(x, y, block, rotation) ? Pal.removeBack : Pal.accentBack);
public void drawArrow(Block block, int x, int y, int rotation){
drawArrow(block, x, y, rotation, validPlace(x, y, block, rotation));
}
public void drawArrow(Block block, int x, int y, int rotation, boolean valid){
Draw.color(!valid ? Pal.removeBack : Pal.accentBack);
Draw.rect(Core.atlas.find("place-arrow"),
x * tilesize + block.offset(),
y * tilesize + block.offset() - 1,
Core.atlas.find("place-arrow").getWidth() * Draw.scl,
Core.atlas.find("place-arrow").getHeight() * Draw.scl, rotation * 90 - 90);
Draw.color(!validPlace(x, y, block, rotation) ? Pal.remove : Pal.accent);
Draw.color(!valid ? Pal.remove : Pal.accent);
Draw.rect(Core.atlas.find("place-arrow"),
x * tilesize + block.offset(),
y * tilesize + block.offset(),

View file

@ -1,12 +1,10 @@
package io.anuke.mindustry.io;
import io.anuke.arc.collection.*;
import io.anuke.arc.util.serialization.*;
import io.anuke.arc.util.serialization.Json.*;
import io.anuke.mindustry.*;
import io.anuke.mindustry.content.*;
import io.anuke.mindustry.game.*;
import io.anuke.mindustry.game.Teams.*;
import io.anuke.mindustry.type.*;
import io.anuke.mindustry.world.*;
@ -98,6 +96,7 @@ public class JsonIO{
}
});
/*
json.setSerializer(TeamData.class, new Serializer<TeamData>(){
@Override
public void write(Json json, TeamData object, Class knownType){
@ -115,7 +114,7 @@ public class JsonIO{
out.brokenBlocks = new LongQueue(blocks);
return out;
}
});
});*/
json.setSerializer(ItemStack.class, new Serializer<ItemStack>(){
@Override

View file

@ -81,8 +81,8 @@ public class MapIO{
super.setBlock(type);
int c = colorFor(Blocks.air, block(), Blocks.air, getTeam());
if(c != black){
walls.drawPixel(x, floors.getHeight() - 1 - y, c);
floors.drawPixel(x, floors.getHeight() - 1 - y + 1, shade);
walls.draw(x, floors.getHeight() - 1 - y, c);
floors.draw(x, floors.getHeight() - 1 - y + 1, shade);
}
}
@ -112,9 +112,9 @@ public class MapIO{
@Override
public Tile create(int x, int y, int floorID, int overlayID, int wallID){
if(overlayID != 0){
floors.drawPixel(x, floors.getHeight() - 1 - y, colorFor(Blocks.air, Blocks.air, content.block(overlayID), Team.derelict));
floors.draw(x, floors.getHeight() - 1 - y, colorFor(Blocks.air, Blocks.air, content.block(overlayID), Team.derelict));
}else{
floors.drawPixel(x, floors.getHeight() - 1 - y, colorFor(content.block(floorID), Blocks.air, Blocks.air, Team.derelict));
floors.draw(x, floors.getHeight() - 1 - y, colorFor(content.block(floorID), Blocks.air, Blocks.air, Team.derelict));
}
if(content.block(overlayID) == Blocks.spawn){
map.spawns ++;
@ -136,7 +136,7 @@ public class MapIO{
for(int x = 0; x < pixmap.getWidth(); x++){
for(int y = 0; y < pixmap.getHeight(); y++){
Tile tile = tiles[x][y];
pixmap.drawPixel(x, pixmap.getHeight() - 1 - y, colorFor(tile.floor(), tile.block(), tile.overlay(), tile.getTeam()));
pixmap.draw(x, pixmap.getHeight() - 1 - y, colorFor(tile.floor(), tile.block(), tile.overlay(), tile.getTeam()));
}
}
return pixmap;

View file

@ -5,8 +5,7 @@ import io.anuke.arc.files.FileHandle;
import io.anuke.arc.util.io.CounterInputStream;
import io.anuke.arc.util.io.FastDeflaterOutputStream;
import io.anuke.mindustry.Vars;
import io.anuke.mindustry.io.versions.Save1;
import io.anuke.mindustry.io.versions.Save2;
import io.anuke.mindustry.io.versions.*;
import io.anuke.mindustry.world.WorldContext;
import java.io.*;
@ -19,7 +18,7 @@ public class SaveIO{
/** Format header. This is the string 'MSAV' in ASCII. */
public static final byte[] header = {77, 83, 65, 86};
public static final IntMap<SaveVersion> versions = new IntMap<>();
public static final Array<SaveVersion> versionArray = Array.with(new Save1(), new Save2());
public static final Array<SaveVersion> versionArray = Array.with(new Save1(), new Save2(), new Save3());
static{
for(SaveVersion version : versionArray){

View file

@ -15,6 +15,7 @@ public class SaveMeta{
public int wave;
public Rules rules;
public StringMap tags;
public String[] mods;
public SaveMeta(int version, long timestamp, long timePlayed, int build, String map, int wave, Rules rules, StringMap tags){
this.version = version;
@ -25,5 +26,6 @@ public class SaveMeta{
this.wave = wave;
this.rules = rules;
this.tags = tags;
this.mods = JsonIO.read(String[].class, tags.get("mods", "[]"));
}
}

View file

@ -6,7 +6,7 @@ import io.anuke.arc.util.io.*;
import io.anuke.mindustry.entities.*;
import io.anuke.mindustry.entities.traits.*;
import io.anuke.mindustry.game.*;
import io.anuke.mindustry.gen.*;
import io.anuke.mindustry.game.Teams.*;
import io.anuke.mindustry.maps.*;
import io.anuke.mindustry.type.*;
import io.anuke.mindustry.world.*;
@ -16,7 +16,7 @@ import java.io.*;
import static io.anuke.mindustry.Vars.*;
public abstract class SaveVersion extends SaveFileReader{
public final int version;
public int version;
//HACK stores the last read build of the save file, valid after read meta call
protected int lastReadBuild;
@ -66,6 +66,7 @@ public abstract class SaveVersion extends SaveFileReader{
"wavetime", state.wavetime,
"stats", JsonIO.write(state.stats),
"rules", JsonIO.write(state.rules),
"mods", JsonIO.write(mods.getModStrings().toArray(String.class)),
"width", world.width(),
"height", world.height()
).merge(tags));
@ -80,6 +81,7 @@ public abstract class SaveVersion extends SaveFileReader{
state.rules = JsonIO.read(Rules.class, map.get("rules", "{}"));
if(state.rules.spawns.isEmpty()) state.rules.spawns = defaultWaves.get();
lastReadBuild = map.getInt("build", -1);
String[] mods = JsonIO.read(String[].class, map.get("mods", "[]"));
Map worldmap = maps.byName(map.get("mapname", "\\\\\\"));
world.setMap(worldmap == null ? new Map(StringMap.of(
@ -206,6 +208,21 @@ public abstract class SaveVersion extends SaveFileReader{
}
public void writeEntities(DataOutput stream) throws IOException{
//write team data with entities.
Array<TeamData> data = state.teams.getActive();
stream.writeInt(data.size);
for(TeamData team : data){
stream.writeInt(team.team.ordinal());
stream.writeInt(team.brokenBlocks.size);
for(BrokenBlock block : team.brokenBlocks){
stream.writeShort(block.x);
stream.writeShort(block.y);
stream.writeShort(block.rotation);
stream.writeShort(block.block);
stream.writeInt(block.config);
}
}
//write entity chunk
int groups = 0;
@ -234,6 +251,16 @@ public abstract class SaveVersion extends SaveFileReader{
}
public void readEntities(DataInput stream) throws IOException{
int teamc = stream.readInt();
for(int i = 0; i < teamc; i++){
Team team = Team.all[stream.readInt()];
TeamData data = state.teams.get(team);
int blocks = stream.readInt();
for(int j = 0; j < blocks; j++){
data.brokenBlocks.addLast(new BrokenBlock(stream.readShort(), stream.readShort(), stream.readShort(), stream.readShort(), stream.readInt()));
}
}
byte groups = stream.readByte();
for(int i = 0; i < groups; i++){
@ -290,17 +317,4 @@ public abstract class SaveVersion extends SaveFileReader{
}
}
}
/** sometimes it's necessary to remap IDs after the content header is read.*/
public void remapContent(){
for(Team team : Team.all){
if(state.teams.isActive(team)){
LongQueue queue = state.teams.get(team).brokenBlocks;
for(int i = 0; i < queue.size; i++){
//remap broken block IDs
queue.set(i, BrokenBlock.block(queue.get(i), content.block(BrokenBlock.block(queue.get(i))).id));
}
}
}
}
}

View file

@ -118,7 +118,7 @@ public class LegacyTypeTable{
public static Supplier[] getTable(int build){
if(build == -1 || build == 81){
//return most recent one since that's probably is; not guaranteed
//return most recent one since that's probably it; not guaranteed
return build81Table;
}else if(build == 80){
return build80Table;

View file

@ -1,16 +1,14 @@
package io.anuke.mindustry.io.versions;
import io.anuke.arc.function.Supplier;
import io.anuke.mindustry.entities.traits.SaveTrait;
import io.anuke.mindustry.io.SaveVersion;
import io.anuke.arc.function.*;
import io.anuke.mindustry.entities.traits.*;
import java.io.DataInput;
import java.io.IOException;
import java.io.*;
public class Save1 extends SaveVersion{
public class Save1 extends Save2{
public Save1(){
super(1);
version = 1;
}
@Override

View file

@ -1,9 +1,35 @@
package io.anuke.mindustry.io.versions;
import io.anuke.mindustry.io.SaveVersion;
import io.anuke.mindustry.entities.traits.*;
import io.anuke.mindustry.game.*;
import io.anuke.mindustry.io.*;
import io.anuke.mindustry.type.*;
import java.io.*;
import static io.anuke.mindustry.Vars.content;
public class Save2 extends SaveVersion{
public Save2(){
super(2);
}
@Override
public void readEntities(DataInput stream) throws IOException{
byte groups = stream.readByte();
for(int i = 0; i < groups; i++){
int amount = stream.readInt();
for(int j = 0; j < amount; j++){
//TODO throw exception on read fail
readChunk(stream, true, in -> {
byte typeid = in.readByte();
byte version = in.readByte();
SaveTrait trait = (SaveTrait)content.<TypeID>getByID(ContentType.typeid, typeid).constructor.get();
trait.readSave(in, version);
});
}
}
}
}

View file

@ -0,0 +1,9 @@
package io.anuke.mindustry.io.versions;
import io.anuke.mindustry.io.*;
public class Save3 extends SaveVersion{
public Save3(){
super(3);
}
}

View file

@ -70,7 +70,7 @@ public class Maps{
* Does not add this map to the map list.
*/
public Map loadInternalMap(String name){
FileHandle file = Core.files.internal("maps/" + name + "." + mapExtension);
FileHandle file = tree.get("maps/" + name + "." + mapExtension);
try{
return MapIO.createMap(file, false);

View file

@ -8,20 +8,20 @@ import io.anuke.arc.scene.ui.*;
import io.anuke.arc.scene.ui.layout.*;
import io.anuke.mindustry.*;
import io.anuke.mindustry.content.*;
import io.anuke.mindustry.game.*;
import io.anuke.mindustry.ui.dialogs.*;
import io.anuke.mindustry.world.*;
import io.anuke.mindustry.world.Block.*;
import io.anuke.mindustry.world.blocks.*;
import static io.anuke.mindustry.Vars.updateEditorOnChange;
public abstract class FilterOption{
public static final Predicate<Block> floorsOnly = b -> (b instanceof Floor && !(b instanceof OverlayFloor)) && Core.atlas.isFound(b.icon(Icon.full));
public static final Predicate<Block> wallsOnly = b -> (!b.synthetic() && !(b instanceof Floor)) && Core.atlas.isFound(b.icon(Icon.full));
public static final Predicate<Block> floorsOptional = b -> b == Blocks.air || ((b instanceof Floor && !(b instanceof OverlayFloor)) && Core.atlas.isFound(b.icon(Icon.full)));
public static final Predicate<Block> wallsOptional = b -> b == Blocks.air || ((!b.synthetic() && !(b instanceof Floor)) && Core.atlas.isFound(b.icon(Icon.full)));
public static final Predicate<Block> wallsOresOptional = b -> b == Blocks.air || (((!b.synthetic() && !(b instanceof Floor)) || (b instanceof OverlayFloor)) && Core.atlas.isFound(b.icon(Icon.full)));
public static final Predicate<Block> oresOnly = b -> b instanceof OverlayFloor && Core.atlas.isFound(b.icon(Icon.full));
public static final Predicate<Block> floorsOnly = b -> (b instanceof Floor && !(b instanceof OverlayFloor)) && Core.atlas.isFound(b.icon(Cicon.full));
public static final Predicate<Block> wallsOnly = b -> (!b.synthetic() && !(b instanceof Floor)) && Core.atlas.isFound(b.icon(Cicon.full));
public static final Predicate<Block> floorsOptional = b -> b == Blocks.air || ((b instanceof Floor && !(b instanceof OverlayFloor)) && Core.atlas.isFound(b.icon(Cicon.full)));
public static final Predicate<Block> wallsOptional = b -> b == Blocks.air || ((!b.synthetic() && !(b instanceof Floor)) && Core.atlas.isFound(b.icon(Cicon.full)));
public static final Predicate<Block> wallsOresOptional = b -> b == Blocks.air || (((!b.synthetic() && !(b instanceof Floor)) || (b instanceof OverlayFloor)) && Core.atlas.isFound(b.icon(Cicon.full)));
public static final Predicate<Block> oresOnly = b -> b instanceof OverlayFloor && Core.atlas.isFound(b.icon(Cicon.full));
public static final Predicate<Block> anyOptional = b -> floorsOnly.test(b) || wallsOnly.test(b) || oresOnly.test(b) || b == Blocks.air;
public abstract void build(Table table);
@ -76,15 +76,15 @@ public abstract class FilterOption{
@Override
public void build(Table table){
table.addButton(b -> b.addImage(supplier.get().icon(Icon.small)).update(i -> ((TextureRegionDrawable)i.getDrawable())
.setRegion(supplier.get() == Blocks.air ? Core.atlas.find("icon-none") : supplier.get().icon(Icon.small))).size(8 * 3), () -> {
table.addButton(b -> b.addImage(supplier.get().icon(Cicon.small)).update(i -> ((TextureRegionDrawable)i.getDrawable())
.setRegion(supplier.get() == Blocks.air ? Core.atlas.find("icon-none") : supplier.get().icon(Cicon.small))).size(8 * 3), () -> {
FloatingDialog dialog = new FloatingDialog("");
dialog.setFillParent(false);
int i = 0;
for(Block block : Vars.content.blocks()){
if(!filter.test(block)) continue;
dialog.cont.addImage(block == Blocks.air ? Core.atlas.find("icon-none-small") : block.icon(Icon.medium)).size(8 * 4).pad(3).get().clicked(() -> {
dialog.cont.addImage(block == Blocks.air ? Core.atlas.find("icon-none-small") : block.icon(Cicon.medium)).size(8 * 4).pad(3).get().clicked(() -> {
consumer.accept(block);
dialog.hide();
changed.run();

View file

@ -0,0 +1,345 @@
package io.anuke.mindustry.mod;
import io.anuke.arc.*;
import io.anuke.arc.audio.*;
import io.anuke.arc.collection.Array;
import io.anuke.arc.collection.*;
import io.anuke.arc.function.*;
import io.anuke.arc.graphics.*;
import io.anuke.arc.util.ArcAnnotate.*;
import io.anuke.arc.util.*;
import io.anuke.arc.util.reflect.Field;
import io.anuke.arc.util.reflect.*;
import io.anuke.arc.util.serialization.*;
import io.anuke.arc.util.serialization.Json.*;
import io.anuke.mindustry.*;
import io.anuke.mindustry.content.*;
import io.anuke.mindustry.entities.Effects.*;
import io.anuke.mindustry.entities.bullet.*;
import io.anuke.mindustry.entities.type.*;
import io.anuke.mindustry.game.*;
import io.anuke.mindustry.gen.*;
import io.anuke.mindustry.mod.Mods.*;
import io.anuke.mindustry.type.*;
import io.anuke.mindustry.world.*;
import java.lang.reflect.*;
@SuppressWarnings("unchecked")
public class ContentParser{
private static final boolean ignoreUnknownFields = true;
private ObjectMap<Class<?>, ContentType> contentTypes = new ObjectMap<>();
private ObjectMap<Class<?>, FieldParser> classParsers = new ObjectMap<Class<?>, FieldParser>(){{
put(BulletType.class, (type, data) -> field(Bullets.class, data));
put(Effect.class, (type, data) -> field(Fx.class, data));
put(StatusEffect.class, (type, data) -> field(StatusEffects.class, data));
put(Loadout.class, (type, data) -> field(Loadouts.class, data));
put(Color.class, (type, data) -> Color.valueOf(data.asString()));
put(Music.class, (type, data) -> {
if(fieldOpt(Musics.class, data) != null) return fieldOpt(Musics.class, data);
String path = "music/" + data.asString() + (Vars.ios ? ".mp3" : ".ogg");
Core.assets.load(path, Music.class);
Core.assets.finishLoadingAsset(path);
return Core.assets.get(path);
});
put(Sound.class, (type, data) -> {
if(fieldOpt(Sounds.class, data) != null) return fieldOpt(Sounds.class, data);
String path = "sounds/" + data.asString() + (Vars.ios ? ".mp3" : ".ogg");
Core.assets.load(path, Sound.class);
Core.assets.finishLoadingAsset(path);
Log.info(Core.assets.get(path));
return Core.assets.get(path);
});
}};
/** Stores things that need to be parsed fully, e.g. reading fields of content.
* This is done to accomodate binding of content names first.*/
private Array<Runnable> reads = new Array<>();
private LoadedMod currentMod;
private Json parser = new Json(){
public <T> T readValue(Class<T> type, Class elementType, JsonValue jsonData){
if(type != null){
if(classParsers.containsKey(type)){
return (T)classParsers.get(type).parse(type, jsonData);
}
if(Content.class.isAssignableFrom(type)){
ContentType ctype = contentTypes.getThrow(type, () -> new IllegalArgumentException("No content type for class: " + type.getSimpleName()));
String prefix = currentMod != null ? currentMod.name + "-" : "";
T one = (T)Vars.content.getByName(ctype, prefix + jsonData.asString());
if(one != null) return one;
return (T)Vars.content.getByName(ctype, jsonData.asString());
}
}
return super.readValue(type, elementType, jsonData);
}
};
private ObjectMap<ContentType, TypeParser<?>> parsers = ObjectMap.of(
ContentType.block, (TypeParser<Block>)(mod, name, value) -> {
//TODO generate dynamically instead of doing.. this
Class<? extends Block> type = resolve(value.getString("type"),
"io.anuke.mindustry.world",
"io.anuke.mindustry.world.blocks",
"io.anuke.mindustry.world.blocks.defense",
"io.anuke.mindustry.world.blocks.defense.turrets",
"io.anuke.mindustry.world.blocks.distribution",
"io.anuke.mindustry.world.blocks.logic",
"io.anuke.mindustry.world.blocks.power",
"io.anuke.mindustry.world.blocks.production",
"io.anuke.mindustry.world.blocks.sandbox",
"io.anuke.mindustry.world.blocks.storage",
"io.anuke.mindustry.world.blocks.units"
);
Block block = type.getDeclaredConstructor(String.class).newInstance(mod + "-" + name);
read(() -> {
if(value.has("consumes")){
for(JsonValue child : value.get("consumes")){
if(child.name.equals("item")){
if(child.isString()){
block.consumes.item(Vars.content.getByName(ContentType.item, child.asString()));
}else{
ItemStack stack = parser.readValue(ItemStack.class, child);
block.consumes.item(stack.item, stack.amount);
}
}else if(child.name.equals("items")){
block.consumes.items(parser.readValue(ItemStack[].class, child));
}else if(child.name.equals("liquid")){
LiquidStack stack = parser.readValue(LiquidStack.class, child);
block.consumes.liquid(stack.liquid, stack.amount);
}else if(child.name.equals("power")){
block.consumes.power(child.asFloat());
}else if(child.name.equals("powerBuffered")){
block.consumes.powerBuffered(child.asFloat());
}else{
throw new IllegalArgumentException("Unknown consumption type: '" + child.name + "' for block '" + block.name + "'.");
}
}
value.remove("consumes");
}
readFields(block, value, true);
//add research tech node
if(value.has("research")){
TechTree.create(Vars.content.getByName(ContentType.block, value.get("research").asString()), block);
}
//make block visible
if(value.has("requirements")){
block.buildVisibility = () -> true;
}
});
return block;
},
ContentType.unit, (TypeParser<UnitType>)(mod, name, value) -> {
Class<BaseUnit> type = resolve(value.getString("type"), "io.anuke.mindustry.entities.type.base");
UnitType unit = new UnitType(mod + "-" + name, supply(type));
read(() -> readFields(unit, value, true));
return unit;
},
ContentType.item, parser(ContentType.item, Item::new),
ContentType.liquid, parser(ContentType.liquid, Liquid::new),
ContentType.mech, parser(ContentType.mech, Mech::new),
ContentType.zone, parser(ContentType.zone, Zone::new)
);
private <T extends Content> TypeParser<T> parser(ContentType type, Function<String, T> constructor){
return (mod, name, value) -> {
T item;
if(Vars.content.getByName(type, name) != null){
item = (T)Vars.content.getByName(type, name);
}else{
item = constructor.get(mod + "-" + name);
}
read(() -> readFields(item, value));
return item;
};
}
/** Call to read a content's extra info later.*/
private void read(Runnable run){
LoadedMod mod = currentMod;
reads.add(() -> {
this.currentMod = mod;
run.run();
});
}
private void init(){
for(ContentType type : ContentType.all){
Array<Content> arr = Vars.content.getBy(type);
if(!arr.isEmpty()){
Class<?> c = arr.first().getClass();
//get base content class, skipping intermediates
while(!(c.getSuperclass() == Content.class || c.getSuperclass() == UnlockableContent.class || Modifier.isAbstract(c.getSuperclass().getModifiers()))){
c = c.getSuperclass();
}
contentTypes.put(c, type);
}
}
}
public void finishParsing(){
reads.each(Runnable::run);
reads.clear();
}
/**
* Parses content from a json file.
* @param name the name of the file without its extension
* @param json the json to parse
* @param type the type of content this is
* @return the content that was parsed
*/
public Content parse(LoadedMod mod, String name, String json, ContentType type) throws Exception{
if(contentTypes.isEmpty()){
init();
}
JsonValue value = parser.fromJson(null, json);
if(!parsers.containsKey(type)){
throw new SerializationException("No parsers for content type '" + type + "'");
}
currentMod = mod;
Content c = parsers.get(type).parse(mod.name, name, value);
c.mod = mod;
checkNulls(c);
return c;
}
private <T> Supplier<T> supply(Class<T> type){
try{
java.lang.reflect.Constructor<T> cons = type.getDeclaredConstructor();
return () -> {
try{
return cons.newInstance();
}catch(Exception e){
throw new RuntimeException(e);
}
};
}catch(Exception e){
throw new RuntimeException(e);
}
}
private Object field(Class<?> type, JsonValue value){
return field(type, value.asString());
}
/** Gets a field from a static class by name, throwing a descriptive exception if not found. */
private Object field(Class<?> type, String name){
try{
Object b = type.getField(name).get(null);
if(b == null) throw new IllegalArgumentException(type.getSimpleName() + ": not found: '" + name + "'");
return b;
}catch(Exception e){
throw new RuntimeException(e);
}
}
private Object fieldOpt(Class<?> type, JsonValue value){
try{
Object b = type.getField(value.asString()).get(null);
if(b == null) return null;
return b;
}catch(Exception e){
return null;
}
}
/** Checks all @NonNull fields in this object, recursively.
* Throws an exception if any are null.*/
private void checkNulls(Object object){
checkNulls(object, new ObjectSet<>());
}
private void checkNulls(Object object, ObjectSet<Object> checked){
checked.add(object);
parser.getFields(object.getClass()).values().toArray().each(field -> {
try{
if(field.field.getType().isPrimitive()) return;
Object obj = field.field.get(object);
if(field.field.isAnnotationPresent(NonNull.class) && field.field.get(object) == null){
throw new RuntimeException("Field '" + field.field.getName() + "' in " + object.getClass().getSimpleName() + " is missing!");
}
if(obj != null && !checked.contains(obj)){
checkNulls(obj, checked);
checked.add(obj);
}
}catch(Exception e){
throw new RuntimeException(e);
}
});
}
private void readFields(Object object, JsonValue jsonMap, boolean stripType){
if(stripType) jsonMap.remove("type");
readFields(object, jsonMap);
}
private void readFields(Object object, JsonValue jsonMap){
Class type = object.getClass();
ObjectMap<String, FieldMetadata> fields = parser.getFields(type);
for(JsonValue child = jsonMap.child; child != null; child = child.next){
FieldMetadata metadata = fields.get(child.name().replace(" ", "_"));
if(metadata == null){
if(ignoreUnknownFields){
if(!child.name.equals("research")){
Log.err("{0}: Ignoring unknown field: " + child.name + " (" + type.getName() + ")", object);
}
continue;
}else{
SerializationException ex = new SerializationException("Field not found: " + child.name + " (" + type.getName() + ")");
ex.addTrace(child.trace());
throw ex;
}
}
Field field = metadata.field;
try{
field.set(object, parser.readValue(field.getType(), metadata.elementType, child));
}catch(ReflectionException ex){
throw new SerializationException("Error accessing field: " + field.getName() + " (" + type.getName() + ")", ex);
}catch(SerializationException ex){
ex.addTrace(field.getName() + " (" + type.getName() + ")");
throw ex;
}catch(RuntimeException runtimeEx){
SerializationException ex = new SerializationException(runtimeEx);
ex.addTrace(child.trace());
ex.addTrace(field.getName() + " (" + type.getName() + ")");
throw ex;
}
}
}
/** Tries to resolve a class from a list of potential class names. */
private <T> Class<T> resolve(String base, String... potentials) throws Exception{
for(String type : potentials){
try{
return (Class<T>)Class.forName(type + '.' + base);
}catch(Exception ignored){
}
}
throw new IllegalArgumentException("Type not found: " + potentials[0]);
}
private interface FieldParser{
Object parse(Class<?> type, JsonValue value);
}
private interface TypeParser<T extends Content>{
T parse(String mod, String name, JsonValue value) throws Exception;
}
}

View file

@ -0,0 +1,32 @@
package io.anuke.mindustry.mod;
import io.anuke.arc.files.*;
import io.anuke.arc.util.*;
import io.anuke.mindustry.*;
public class Mod{
/** @return the config file for this plugin, as the file 'mods/[plugin-name]/config.json'.*/
public FileHandle getConfig(){
return Vars.mods.getConfig(this);
}
/** Called after all plugins have been created and commands have been registered.*/
public void init(){
}
/** Create any content needed here. */
public void loadContent(){
}
/** Register any commands to be used on the server side, e.g. from the console. */
public void registerServerCommands(CommandHandler handler){
}
/** Register any commands to be used on the client side, e.g. sent from an in-game player.. */
public void registerClientCommands(CommandHandler handler){
}
}

View file

@ -0,0 +1,419 @@
package io.anuke.mindustry.mod;
import io.anuke.arc.*;
import io.anuke.arc.assets.*;
import io.anuke.arc.collection.*;
import io.anuke.arc.files.*;
import io.anuke.arc.function.*;
import io.anuke.arc.graphics.*;
import io.anuke.arc.graphics.Pixmap.*;
import io.anuke.arc.graphics.Texture.*;
import io.anuke.arc.graphics.g2d.*;
import io.anuke.arc.graphics.g2d.TextureAtlas.*;
import io.anuke.arc.util.ArcAnnotate.*;
import io.anuke.arc.util.*;
import io.anuke.arc.util.io.*;
import io.anuke.arc.util.serialization.*;
import io.anuke.mindustry.game.*;
import io.anuke.mindustry.gen.*;
import io.anuke.mindustry.plugin.*;
import io.anuke.mindustry.type.*;
import java.io.*;
import java.net.*;
import static io.anuke.mindustry.Vars.*;
public class Mods implements Loadable{
private Json json = new Json();
private ContentParser parser = new ContentParser();
private ObjectMap<String, Array<FileHandle>> bundles = new ObjectMap<>();
private ObjectSet<String> specialFolders = ObjectSet.with("bundles", "sprites");
private int totalSprites;
private PixmapPacker packer;
private Array<LoadedMod> loaded = new Array<>();
private Array<LoadedMod> disabled = new Array<>();
private ObjectMap<Class<?>, ModMeta> metas = new ObjectMap<>();
private boolean requiresReload;
/** Returns a file named 'config.json' in a special folder for the specified plugin.
* Call this in init(). */
public FileHandle getConfig(Mod mod){
ModMeta load = metas.get(mod.getClass());
if(load == null) throw new IllegalArgumentException("Mod is not loaded yet (or missing)!");
return modDirectory.child(load.name).child("config.json");
}
/** @return the loaded mod found by class, or null if not found. */
public @Nullable
LoadedMod getMod(Class<? extends Mod> type){
return loaded.find(l -> l.mod.getClass() == type);
}
/** Imports an external mod file.*/
public void importMod(FileHandle file) throws IOException{
FileHandle dest = modDirectory.child(file.name());
if(dest.exists()){
throw new IOException("A mod with the same filename already exists!");
}
file.copyTo(dest);
try{
loaded.add(loadMod(file));
requiresReload = true;
}catch(IOException e){
dest.delete();
throw e;
}catch(Throwable t){
dest.delete();
throw new IOException(t);
}
}
/** Repacks all in-game sprites. */
@Override
public void loadAsync(){
if(loaded.isEmpty()) return;
packer = new PixmapPacker(2048, 2048, Format.RGBA8888, 2, true);
for(LoadedMod mod : loaded){
int[] packed = {0};
boolean[] failed = {false};
mod.root.child("sprites").walk(file -> {
if(failed[0]) return;
if(file.extension().equals("png")){
try(InputStream stream = file.read()){
byte[] bytes = Streams.copyStreamToByteArray(stream, Math.max((int)file.length(), 512));
Pixmap pixmap = new Pixmap(bytes, 0, bytes.length);
packer.pack(mod.name + "-" + file.nameWithoutExtension(), pixmap);
pixmap.dispose();
packed[0] ++;
totalSprites ++;
}catch(IOException e){
failed[0] = true;
Core.app.post(() -> {
Log.err("Error packing images for mod: {0}", mod.meta.name);
e.printStackTrace();
if(!headless) ui.showException(e);
});
}
}
});
Log.info("Packed {0} images for mod '{1}'.", packed[0], mod.meta.name);
}
}
@Override
public void loadSync(){
if(packer == null) return;
Texture editor = Core.atlas.find("clear-editor").getTexture();
PixmapPacker editorPacker = new PixmapPacker(2048, 2048, Format.RGBA8888, 2, true);
for(AtlasRegion region : Core.atlas.getRegions()){
if(region.getTexture() == editor){
editorPacker.pack(region.name, Core.atlas.getPixmap(region).crop());
}
}
//get textures packed
if(totalSprites > 0){
TextureFilter filter = Core.settings.getBool("linear") ? TextureFilter.Linear : TextureFilter.Nearest;
packer.updateTextureAtlas(Core.atlas, filter, filter, false);
//generate new icons
for(Array<Content> arr : content.getContentMap()){
arr.each(c -> {
if(c instanceof UnlockableContent && c.mod != null){
UnlockableContent u = (UnlockableContent)c;
u.createIcons(packer, editorPacker);
}
});
}
editorPacker.updateTextureAtlas(Core.atlas, filter, filter, false);
packer.updateTextureAtlas(Core.atlas, filter, filter, false);
}
packer.dispose();
packer = null;
}
/** Removes a mod file and marks it for requiring a restart. */
public void removeMod(LoadedMod mod){
if(mod.file.isDirectory()){
mod.file.deleteDirectory();
}else{
mod.file.delete();
}
loaded.remove(mod);
requiresReload = true;
}
public boolean requiresReload(){
return requiresReload;
}
/** Loads all mods from the folder, but does call any methods on them.*/
public void load(){
for(FileHandle file : modDirectory.list()){
if(!file.extension().equals("jar") && !file.extension().equals("zip") && !(file.isDirectory() && file.child("mod.json").exists())) continue;
try{
LoadedMod mod = loadMod(file);
if(mod.enabled()){
loaded.add(mod);
}else{
disabled.add(mod);
}
}catch(IllegalArgumentException ignored){
}catch(Exception e){
Log.err("Failed to load plugin file {0}. Skipping.", file);
e.printStackTrace();
}
}
//sort mods to make sure servers handle them properly.
loaded.sort(Structs.comparing(m -> m.name));
buildFiles();
}
private void buildFiles(){
for(LoadedMod mod : loaded){
for(FileHandle file : mod.root.list()){
//ignore special folders like bundles or sprites
if(file.isDirectory() && !specialFolders.contains(file.name())){
//TODO calling child/parent on these files will give you gibberish; create wrapper class.
file.walk(f -> tree.addFile(mod.file.isDirectory() ? f.path().substring(1 + mod.file.path().length()) : f.path(), f));
}
}
//load up bundles.
FileHandle folder = mod.root.child("bundles");
if(folder.exists()){
for(FileHandle file : folder.list()){
if(file.name().startsWith("bundle") && file.extension().equals("properties")){
String name = file.nameWithoutExtension();
bundles.getOr(name, Array::new).add(file);
}
}
}
}
//add new keys to each bundle
I18NBundle bundle = Core.bundle;
while(bundle != null){
String str = bundle.getLocale().toString();
String locale = "bundle" + (str.isEmpty() ? "" : "_" + str);
for(FileHandle file : bundles.getOr(locale, Array::new)){
try{
PropertiesUtils.load(bundle.getProperties(), file.reader());
}catch(Exception e){
throw new RuntimeException("Error loading bundle: " + file + "/" + locale, e);
}
}
bundle = bundle.getParent();
}
}
/** Reloads all mod content. How does this even work? I refuse to believe that it functions correctly.*/
public void reloadContent(){
//epic memory leak
Core.atlas = new TextureAtlas(Core.files.internal("sprites/sprites.atlas"));
loaded.clear();
disabled.clear();
load();
buildFiles();
Musics.dispose();
Sounds.dispose();
Musics.load();
Sounds.load();
Core.assets.finishLoading();
content.clear();
content.createContent();
loadAsync();
loadSync();
content.init();
content.load();
content.loadColors();
data.load();
requiresReload = false;
}
/** Creates all the content found in mod files. */
public void loadContent(){
for(LoadedMod mod : loaded){
if(mod.root.child("content").exists()){
FileHandle contentRoot = mod.root.child("content");
for(ContentType type : ContentType.all){
FileHandle folder = contentRoot.child(type.name().toLowerCase() + "s");
if(folder.exists()){
for(FileHandle file : folder.list()){
if(file.extension().equals("json")){
try{
//this binds the content but does not load it entirely
Content loaded = parser.parse(mod, file.nameWithoutExtension(), file.readString(), type);
Log.info("[{0}] Loaded '{1}'.", mod.meta.name, loaded);
}catch(Exception e){
throw new RuntimeException("Failed to parse content file '" + file + "' for mod '" + mod.meta.name + "'.", e);
}
}
}
}
}
}
}
//this finishes parsing content fields
parser.finishParsing();
each(Mod::loadContent);
}
/** @return all loaded mods. */
public Array<LoadedMod> all(){
return loaded;
}
/** @return all disabled mods. */
public Array<LoadedMod> disabled(){
return disabled;
}
/** @return a list of mod names only, without versions. */
public Array<String> getModNames(){
return loaded.select(l -> !l.meta.hidden).map(l -> l.name + ":" + l.meta.version);
}
/** @return a list of mods and versions, in the format name:version. */
public Array<String> getModStrings(){
return loaded.select(l -> !l.meta.hidden).map(l -> l.name + ":" + l.meta.version);
}
/** Makes a mod enabled or disabled. shifts it.*/
public void setEnabled(LoadedMod mod, boolean enabled){
if(mod.enabled() != enabled){
Core.settings.putSave(mod.name + "-enabled", enabled);
requiresReload = true;
if(!enabled){
loaded.remove(mod);
disabled.add(mod);
}else{
loaded.add(mod);
disabled.remove(mod);
}
}
}
/** @return the mods that the client is missing.
* The inputted array is changed to contain the extra mods that the client has but the server doesn't.*/
public Array<String> getIncompatibility(Array<String> out){
Array<String> mods = getModStrings();
Array<String> result = mods.copy();
for(String mod : mods){
if(out.remove(mod)){
result.remove(mod);
}
}
return result;
}
/** Iterates through each mod with a main class.*/
public void each(Consumer<Mod> cons){
loaded.each(p -> p.mod != null, p -> cons.accept(p.mod));
}
/** Loads a mod file+meta, but does not add it to the list.
* Note that directories can be loaded as mods.*/
private LoadedMod loadMod(FileHandle sourceFile) throws Exception{
FileHandle zip = sourceFile.isDirectory() ? sourceFile : new ZipFileHandle(sourceFile);
if(zip.list().length == 1 && zip.list()[0].isDirectory()){
zip = zip.list()[0];
}
FileHandle metaf = zip.child("mod.json").exists() ? zip.child("mod.json") : zip.child("plugin.json");
if(!metaf.exists()){
Log.warn("Mod {0} doesn't have a 'mod.json'/'plugin.json' file, skipping.", sourceFile);
throw new IllegalArgumentException("No mod.json found.");
}
ModMeta meta = json.fromJson(ModMeta.class, metaf.readString());
String camelized = meta.name.replace(" ", "");
String mainClass = meta.main == null ? camelized.toLowerCase() + "." + camelized + "Mod" : meta.main;
String baseName = meta.name.toLowerCase().replace(" ", "-");
if(loaded.contains(m -> m.name.equals(baseName)) || disabled.contains(m -> m.name.equals(baseName))){
throw new IllegalArgumentException("A mod with the name '" + baseName + "' is already imported.");
}
Mod mainMod;
FileHandle mainFile = zip;
String[] path = (mainClass.replace('.', '/') + ".class").split("/");
for(String str : path){
if(!str.isEmpty()){
mainFile = mainFile.child(str);
}
}
//make sure the main class exists before loading it; if it doesn't just don't put it there
if(mainFile.exists()){
//other platforms don't have standard java class loaders
if(!headless && Version.build != -1){
throw new IllegalArgumentException("Java class mods are currently unsupported outside of custom builds.");
}
URLClassLoader classLoader = new URLClassLoader(new URL[]{sourceFile.file().toURI().toURL()}, ClassLoader.getSystemClassLoader());
Class<?> main = classLoader.loadClass(mainClass);
metas.put(main, meta);
mainMod = (Mod)main.getDeclaredConstructor().newInstance();
}else{
mainMod = null;
}
//all plugins are hidden implicitly
if(mainMod instanceof Plugin){
meta.hidden = true;
}
return new LoadedMod(sourceFile, zip, mainMod, meta);
}
/** Represents a plugin that has been loaded from a jar file.*/
public static class LoadedMod{
/** The location of this mod's zip file/folder on the disk. */
public final FileHandle file;
/** The root zip file; points to the contents of this mod. In the case of folders, this is the same as the mod's file. */
public final FileHandle root;
/** The mod's main class; may be null. */
public final @Nullable Mod mod;
/** Internal mod name. Used for textures. */
public final String name;
/** This mod's metadata. */
public final ModMeta meta;
public LoadedMod(FileHandle file, FileHandle root, Mod mod, ModMeta meta){
this.root = root;
this.file = file;
this.mod = mod;
this.meta = meta;
this.name = meta.name.toLowerCase().replace(" ", "-");
}
public boolean enabled(){
return Core.settings.getBool(name + "-enabled", true);
}
}
/** Plugin metadata information.*/
public static class ModMeta{
public String name, author, description, version, main;
public String[] dependencies = {}; //TODO implement
/** Hidden mods are only server-side or client-side, and do not support adding new content. */
public boolean hidden;
}
}

View file

@ -26,7 +26,7 @@ public class CrashSender{
exception.printStackTrace();
//don't create crash logs for custom builds, as it's expected
if(Version.build == -1) return;
if(Version.build == -1 || (System.getProperty("user.name").equals("anuke") && "release".equals(Version.modifier))) return;
//attempt to load version regardless
if(Version.number == 0){
@ -93,6 +93,7 @@ public class CrashSender{
ex(() -> value.addChild("versionNumber", new JsonValue(Version.number)));
ex(() -> value.addChild("versionModifier", new JsonValue(Version.modifier)));
ex(() -> value.addChild("build", new JsonValue(Version.build)));
ex(() -> value.addChild("revision", new JsonValue(Version.revision)));
ex(() -> value.addChild("net", new JsonValue(fn)));
ex(() -> value.addChild("server", new JsonValue(fs)));
ex(() -> value.addChild("players", new JsonValue(Vars.playerGroup.size())));
@ -143,8 +144,7 @@ public class CrashSender{
private static void ex(Runnable r){
try{
r.run();
}catch(Throwable t){
t.printStackTrace();
}catch(Throwable ignored){
}
}
}

View file

@ -1,10 +1,10 @@
package io.anuke.mindustry.net;
import io.anuke.annotations.Annotations.*;
import io.anuke.arc.*;
import io.anuke.arc.collection.*;
import io.anuke.arc.function.*;
import io.anuke.arc.util.*;
import io.anuke.arc.util.ArcAnnotate.*;
import io.anuke.arc.util.pooling.*;
import io.anuke.mindustry.gen.*;
import io.anuke.mindustry.net.Packets.*;
@ -21,7 +21,8 @@ public class Net{
private boolean server;
private boolean active;
private boolean clientLoaded;
private @Nullable StreamBuilder currentStream;
private @Nullable
StreamBuilder currentStream;
private final Array<Object> packetQueue = new Array<>();
private final ObjectMap<Class<?>, Consumer> clientListeners = new ObjectMap<>();

View file

@ -1,7 +1,7 @@
package io.anuke.mindustry.net;
import io.anuke.annotations.Annotations.*;
import io.anuke.arc.util.*;
import io.anuke.arc.util.ArcAnnotate.*;
import io.anuke.mindustry.entities.type.*;
import io.anuke.mindustry.gen.*;
import io.anuke.mindustry.net.Administration.*;
@ -15,7 +15,8 @@ import static io.anuke.mindustry.Vars.netServer;
public abstract class NetConnection{
public final String address;
public boolean mobile, modclient;
public @Nullable Player player;
public @Nullable
Player player;
/** ID of last recieved client snapshot. */
public int lastRecievedClientSnapshot = -1;
@ -48,7 +49,7 @@ public abstract class NetConnection{
/** Kick with an arbitrary reason. */
public void kick(String reason){
Log.info("Kicking connection {0}; Reason: {1}", address, reason);
Log.info("Kicking connection {0}; Reason: {1}", address, reason.replace("\n", " "));
if(player != null && player.uuid != null){
PlayerInfo info = netServer.admins.getInfo(player.uuid);

View file

@ -1,6 +1,7 @@
package io.anuke.mindustry.net;
import io.anuke.arc.Core;
import io.anuke.arc.collection.*;
import io.anuke.arc.util.serialization.Base64Coder;
import io.anuke.mindustry.game.Version;
import io.anuke.mindustry.io.TypeIO;
@ -65,6 +66,7 @@ public class Packets{
public static class ConnectPacket implements Packet{
public int version;
public String versionType;
public Array<String> mods;
public String name, uuid, usid;
public boolean mobile;
public int color;
@ -78,6 +80,10 @@ public class Packets{
buffer.put(mobile ? (byte)1 : 0);
buffer.putInt(color);
buffer.put(Base64Coder.decode(uuid));
buffer.putInt(mods.size);
for(int i = 0; i < mods.size; i++){
TypeIO.writeString(buffer, mods.get(i));
}
}
@Override
@ -91,6 +97,11 @@ public class Packets{
byte[] idbytes = new byte[8];
buffer.get(idbytes);
uuid = new String(Base64Coder.encode(idbytes));
int totalMods = buffer.getInt();
mods = new Array<>(totalMods);
for(int i = 0; i < totalMods; i++){
mods.add(TypeIO.readString(buffer));
}
}
}

View file

@ -1,28 +1,7 @@
package io.anuke.mindustry.plugin;
import io.anuke.arc.files.*;
import io.anuke.arc.util.*;
import io.anuke.mindustry.*;
import io.anuke.mindustry.mod.*;
public abstract class Plugin{
public abstract class Plugin extends Mod{
/** @return the config file for this plugin, as the file 'plugins/[plugin-name]/config.json'.*/
public FileHandle getConfig(){
return Vars.plugins.getConfig(this);
}
/** Called after all plugins have been created and commands have been registered.*/
public void init(){
}
/** Register any commands to be used on the server side, e.g. from the console. */
public void registerServerCommands(CommandHandler handler){
}
/** Register any commands to be used on the client side, e.g. sent from an in-game player.. */
public void registerClientCommands(CommandHandler handler){
}
}

View file

@ -1,93 +0,0 @@
package io.anuke.mindustry.plugin;
import io.anuke.annotations.Annotations.*;
import io.anuke.arc.collection.*;
import io.anuke.arc.files.*;
import io.anuke.arc.function.*;
import io.anuke.arc.util.*;
import io.anuke.mindustry.io.*;
import java.net.*;
import static io.anuke.mindustry.Vars.pluginDirectory;
public class Plugins{
private Array<LoadedPlugin> loaded = new Array<>();
private ObjectMap<Class<?>, PluginMeta> metas = new ObjectMap<>();
/** Returns a file named 'config.json' in a special folder for the specified plugin.
* Call this in init(). */
public FileHandle getConfig(Plugin plugin){
PluginMeta load = metas.get(plugin.getClass());
if(load == null) throw new IllegalArgumentException("Plugin is not loaded yet (or missing)!");
return pluginDirectory.child(load.name).child("config.json");
}
/** @return the loaded plugin found by class, or null if not found. */
public @Nullable LoadedPlugin getPlugin(Class<? extends Plugin> type){
return loaded.find(l -> l.plugin.getClass() == type);
}
/** Loads all plugins from the folder, but does call any methods on them.*/
public void load(){
for(FileHandle file : pluginDirectory.list()){
if(!file.extension().equals("jar")) continue;
try{
loaded.add(loadPlugin(file));
}catch(IllegalArgumentException ignored){
}catch(Exception e){
Log.err("Failed to load plugin file {0}. Skipping.", file);
e.printStackTrace();
}
}
}
/** @return all loaded plugins. */
public Array<LoadedPlugin> all(){
return loaded;
}
/** Iterates through each plugin.*/
public void each(Consumer<Plugin> cons){
loaded.each(p -> cons.accept(p.plugin));
}
private LoadedPlugin loadPlugin(FileHandle jar) throws Exception{
FileHandle zip = new ZipFileHandle(jar);
FileHandle metaf = zip.child("plugin.json");
if(!metaf.exists()){
Log.warn("Plugin {0} doesn't have a 'plugin.json' file, skipping.", jar);
throw new IllegalArgumentException();
}
PluginMeta meta = JsonIO.read(PluginMeta.class, metaf.readString());
URLClassLoader classLoader = new URLClassLoader(new URL[]{jar.file().toURI().toURL()}, ClassLoader.getSystemClassLoader());
Class<?> main = classLoader.loadClass(meta.main);
metas.put(main, meta);
return new LoadedPlugin(jar, zip, (Plugin)main.getDeclaredConstructor().newInstance(), meta);
}
/** Represents a plugin that has been loaded from a jar file.*/
public static class LoadedPlugin{
public final FileHandle jarFile;
public final FileHandle zipRoot;
public final Plugin plugin;
public final PluginMeta meta;
public LoadedPlugin(FileHandle jarFile, FileHandle zipRoot, Plugin plugin, PluginMeta meta){
this.zipRoot = zipRoot;
this.jarFile = jarFile;
this.plugin = plugin;
this.meta = meta;
}
}
/** Plugin metadata information.*/
public static class PluginMeta{
public String name, author, main, description;
public String version;
}
}

View file

@ -13,5 +13,7 @@ public enum ContentType{
effect,
zone,
loadout,
typeid
typeid;
public static final ContentType[] all = values();
}

Some files were not shown because too many files have changed in this diff Show more