Content patch import dialog & server support
This commit is contained in:
@@ -470,6 +470,13 @@ editor.rules = Rules
|
|||||||
editor.generation = Generation
|
editor.generation = Generation
|
||||||
editor.objectives = Objectives
|
editor.objectives = Objectives
|
||||||
editor.locales = Locale Bundles
|
editor.locales = Locale Bundles
|
||||||
|
editor.patches = Content Patches
|
||||||
|
editor.patch: Patchset: {0}
|
||||||
|
editor.patches.none = [lightgray]No patchsets loaded.
|
||||||
|
editor.patches.errors = Patchset Errors
|
||||||
|
editor.patches.importerror = Failed to import patchset
|
||||||
|
editor.patches.delete.confirm = Are you sure you want to delete this patchset?
|
||||||
|
editor.patch.fields = {0} fields
|
||||||
editor.worldprocessors = World Processors
|
editor.worldprocessors = World Processors
|
||||||
editor.worldprocessors.editname = Edit Name
|
editor.worldprocessors.editname = Edit Name
|
||||||
editor.worldprocessors.none = [lightgray]No world processor blocks found!\nAdd one in the map editor, or use the \ue813 Add button below.
|
editor.worldprocessors.none = [lightgray]No world processor blocks found!\nAdd one in the map editor, or use the \ue813 Add button below.
|
||||||
|
|||||||
BIN
core/assets/fonts/monospace.woff
Normal file
BIN
core/assets/fonts/monospace.woff
Normal file
Binary file not shown.
@@ -43,12 +43,12 @@ public class GameState{
|
|||||||
public Attributes envAttrs = new Attributes();
|
public Attributes envAttrs = new Attributes();
|
||||||
/** Team data. Gets reset every new game. */
|
/** Team data. Gets reset every new game. */
|
||||||
public Teams teams = new Teams();
|
public Teams teams = new Teams();
|
||||||
|
/** Handles JSON edits of game content. */
|
||||||
|
public ContentPatcher patcher = new ContentPatcher();
|
||||||
/** Number of enemies in the game; only used clientside in servers. */
|
/** Number of enemies in the game; only used clientside in servers. */
|
||||||
public int enemies;
|
public int enemies;
|
||||||
/** Map being playtested (not edited!) */
|
/** Map being playtested (not edited!) */
|
||||||
public @Nullable Map playtestingMap;
|
public @Nullable Map playtestingMap;
|
||||||
/** Null if not content patches have been applied. */
|
|
||||||
public @Nullable ContentPatcher patcher;
|
|
||||||
/** Current game state. */
|
/** Current game state. */
|
||||||
private State state = State.menu;
|
private State state = State.menu;
|
||||||
|
|
||||||
|
|||||||
@@ -260,10 +260,7 @@ public class Logic implements ApplicationListener{
|
|||||||
|
|
||||||
public void reset(){
|
public void reset(){
|
||||||
State prev = state.getState();
|
State prev = state.getState();
|
||||||
if(state.patcher != null){
|
state.patcher.unapply();
|
||||||
state.patcher.unapply();
|
|
||||||
state.patcher = null;
|
|
||||||
}
|
|
||||||
//recreate gamestate - sets state to menu
|
//recreate gamestate - sets state to menu
|
||||||
state = new GameState();
|
state = new GameState();
|
||||||
//fire change event, since it was technically changed
|
//fire change event, since it was technically changed
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ public class MapInfoDialog extends BaseDialog{
|
|||||||
private MapObjectivesDialog objectives = new MapObjectivesDialog();
|
private MapObjectivesDialog objectives = new MapObjectivesDialog();
|
||||||
private MapLocalesDialog locales = new MapLocalesDialog();
|
private MapLocalesDialog locales = new MapLocalesDialog();
|
||||||
private MapProcessorsDialog processors = new MapProcessorsDialog();
|
private MapProcessorsDialog processors = new MapProcessorsDialog();
|
||||||
|
private MapPatchesDialog patches = new MapPatchesDialog();
|
||||||
|
|
||||||
public MapInfoDialog(){
|
public MapInfoDialog(){
|
||||||
super("@editor.mapinfo");
|
super("@editor.mapinfo");
|
||||||
@@ -33,7 +34,7 @@ public class MapInfoDialog extends BaseDialog{
|
|||||||
cont.clear();
|
cont.clear();
|
||||||
|
|
||||||
ObjectMap<String, String> tags = editor.tags;
|
ObjectMap<String, String> tags = editor.tags;
|
||||||
|
|
||||||
cont.pane(t -> {
|
cont.pane(t -> {
|
||||||
t.add("@editor.mapname").padRight(8).left();
|
t.add("@editor.mapname").padRight(8).left();
|
||||||
t.defaults().padTop(15);
|
t.defaults().padTop(15);
|
||||||
@@ -113,6 +114,16 @@ public class MapInfoDialog extends BaseDialog{
|
|||||||
hide();
|
hide();
|
||||||
processors.show();
|
processors.show();
|
||||||
}).marginLeft(10f);
|
}).marginLeft(10f);
|
||||||
|
|
||||||
|
r.row();
|
||||||
|
|
||||||
|
r.button("@editor.patches", Icon.file, style, () -> {
|
||||||
|
hide();
|
||||||
|
patches.show();
|
||||||
|
}).marginLeft(10f);
|
||||||
|
|
||||||
|
//empty space
|
||||||
|
r.add().marginLeft(10f);
|
||||||
}).colspan(2).center();
|
}).colspan(2).center();
|
||||||
|
|
||||||
name.change();
|
name.change();
|
||||||
|
|||||||
156
core/src/mindustry/editor/MapPatchesDialog.java
Normal file
156
core/src/mindustry/editor/MapPatchesDialog.java
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
package mindustry.editor;
|
||||||
|
|
||||||
|
import arc.*;
|
||||||
|
import arc.func.*;
|
||||||
|
import arc.scene.ui.TextButton.*;
|
||||||
|
import arc.scene.ui.layout.*;
|
||||||
|
import arc.struct.*;
|
||||||
|
import arc.util.*;
|
||||||
|
import arc.util.serialization.*;
|
||||||
|
import mindustry.*;
|
||||||
|
import mindustry.gen.*;
|
||||||
|
import mindustry.ui.*;
|
||||||
|
import mindustry.ui.dialogs.*;
|
||||||
|
|
||||||
|
import static mindustry.Vars.*;
|
||||||
|
|
||||||
|
public class MapPatchesDialog extends BaseDialog{
|
||||||
|
private Table list;
|
||||||
|
|
||||||
|
public MapPatchesDialog(){
|
||||||
|
super("@editor.patches");
|
||||||
|
|
||||||
|
shown(this::setup);
|
||||||
|
|
||||||
|
addCloseButton();
|
||||||
|
buttons.button("@add", Icon.add, () -> showImport(this::addPatch)).size(210f, 64f);
|
||||||
|
|
||||||
|
cont.top();
|
||||||
|
getCell(cont).grow();
|
||||||
|
|
||||||
|
cont.pane(t -> list = t);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setup(){
|
||||||
|
list.clearChildren();
|
||||||
|
var patches = state.patcher.patches;
|
||||||
|
|
||||||
|
if(patches.isEmpty()){
|
||||||
|
list.add("@editor.patches.none");
|
||||||
|
}else{
|
||||||
|
Table t = list;
|
||||||
|
|
||||||
|
t.defaults().pad(4f);
|
||||||
|
float h = 50f;
|
||||||
|
for(var patch : patches){
|
||||||
|
int fields = countFields(patch.json);
|
||||||
|
|
||||||
|
if(patch.warnings.size > 0){
|
||||||
|
t.button(Icon.warning, Styles.graySquarei, iconMed, () -> {
|
||||||
|
BaseDialog dialog = new BaseDialog("@editor.patches.errors");
|
||||||
|
dialog.cont.top().pane(p -> {
|
||||||
|
p.top();
|
||||||
|
|
||||||
|
for(var warning : patch.warnings){
|
||||||
|
p.table(Styles.grayPanel, in -> {
|
||||||
|
in.add(warning, Styles.monoLabel).grow().wrap();
|
||||||
|
}).margin(6f).growX().pad(3f).row();
|
||||||
|
}
|
||||||
|
}).grow();
|
||||||
|
dialog.addCloseButton();
|
||||||
|
dialog.show();
|
||||||
|
}).size(h);
|
||||||
|
}else{
|
||||||
|
t.add().size(h);
|
||||||
|
}
|
||||||
|
|
||||||
|
t.button((patch.name.isEmpty() ? "<unnamed>\n" : "[accent]" + patch.name + "\n") + "[lightgray][[" + Core.bundle.format("editor.patch.fields", fields) + "]", Styles.grayt, () -> {
|
||||||
|
BaseDialog dialog = new BaseDialog(Core.bundle.format("editor.patch", patch.name.isEmpty() ? "<unnamed>" : patch.name));
|
||||||
|
dialog.cont.top().pane(p -> {
|
||||||
|
p.top();
|
||||||
|
p.table(Styles.grayPanel, in -> {
|
||||||
|
in.add(patch.patch.replaceAll("\t", " "), Styles.monoLabel).grow().wrap().left().labelAlign(Align.left);
|
||||||
|
}).margin(6f).growX().pad(5f).row();
|
||||||
|
}).grow();
|
||||||
|
dialog.addCloseButton();
|
||||||
|
dialog.show();
|
||||||
|
}).size(mobile ? 390f : 450f, h).margin(10f).with(b -> {
|
||||||
|
b.getLabel().setAlignment(Align.left, Align.left);
|
||||||
|
});
|
||||||
|
|
||||||
|
t.button(Icon.refresh, Styles.graySquarei, Vars.iconMed, () -> {
|
||||||
|
showImport(str -> addPatch(str, patches.indexOf(patch)));
|
||||||
|
}).size(h);
|
||||||
|
|
||||||
|
t.button(Icon.trash, Styles.graySquarei, iconMed, () -> {
|
||||||
|
ui.showConfirm("@editor.patches.delete.confirm", () -> {
|
||||||
|
patches.remove(patch);
|
||||||
|
setup();
|
||||||
|
});
|
||||||
|
}).size(h);
|
||||||
|
|
||||||
|
t.row();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void showImport(Cons<String> handler){
|
||||||
|
BaseDialog dialog = new BaseDialog("@editor.import");
|
||||||
|
dialog.cont.pane(p -> {
|
||||||
|
p.margin(10f);
|
||||||
|
p.table(Tex.button, t -> {
|
||||||
|
TextButtonStyle style = Styles.flatt;
|
||||||
|
t.defaults().size(280f, 60f).left();
|
||||||
|
t.row();
|
||||||
|
t.button("@schematic.copy.import", Icon.copy, style, () -> {
|
||||||
|
dialog.hide();
|
||||||
|
handler.get(Core.app.getClipboardText());
|
||||||
|
}).marginLeft(12f).disabled(b -> Core.app.getClipboardText() == null);
|
||||||
|
t.row();
|
||||||
|
t.button("@schematic.importfile", Icon.download, style, () -> platform.showMultiFileChooser(file -> {
|
||||||
|
dialog.hide();
|
||||||
|
handler.get(file.readString());
|
||||||
|
}, "json", "hjson", "json5")).marginLeft(12f);
|
||||||
|
t.row();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
dialog.addCloseButton();
|
||||||
|
dialog.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
void addPatch(String patch){
|
||||||
|
addPatch(patch, -1);
|
||||||
|
}
|
||||||
|
|
||||||
|
void addPatch(String patch, int replaceIndex){
|
||||||
|
var oldPatches = state.patcher.patches.copy();
|
||||||
|
try{
|
||||||
|
Jval.read(patch); //validation
|
||||||
|
Seq<String> patches = state.patcher.patches.map(p -> p.patch);
|
||||||
|
if(replaceIndex == -1){
|
||||||
|
patches.add(patch);
|
||||||
|
}else{
|
||||||
|
patches.set(replaceIndex, patch);
|
||||||
|
}
|
||||||
|
state.patcher.apply(patches);
|
||||||
|
|
||||||
|
setup();
|
||||||
|
}catch(Exception e){
|
||||||
|
state.patcher.patches.set(oldPatches);
|
||||||
|
ui.showException("@editor.patches.importerror", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int countFields(JsonValue value){
|
||||||
|
if(value.isObject() || value.isArray()){
|
||||||
|
int sum = 0;
|
||||||
|
for(var child : value){
|
||||||
|
sum += countFields(child);
|
||||||
|
}
|
||||||
|
return Math.max(sum, 1);
|
||||||
|
}else{
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package mindustry.game;
|
package mindustry.game;
|
||||||
|
|
||||||
import arc.math.geom.*;
|
import arc.math.geom.*;
|
||||||
|
import arc.struct.*;
|
||||||
import arc.util.*;
|
import arc.util.*;
|
||||||
import mindustry.core.GameState.*;
|
import mindustry.core.GameState.*;
|
||||||
import mindustry.ctype.*;
|
import mindustry.ctype.*;
|
||||||
@@ -102,6 +103,15 @@ public class EventType{
|
|||||||
/** Called when a game begins and the world tiles are initiated. About to updates tile proximity and sets up physics for the world(Before WorldLoadEvent) */
|
/** Called when a game begins and the world tiles are initiated. About to updates tile proximity and sets up physics for the world(Before WorldLoadEvent) */
|
||||||
public static class WorldLoadEndEvent{}
|
public static class WorldLoadEndEvent{}
|
||||||
|
|
||||||
|
/** Called when a save loads custom patches. {@link #patches} can be modified in the event handler. */
|
||||||
|
public static class ContentPatchLoadEvent{
|
||||||
|
public final Seq<String> patches;
|
||||||
|
|
||||||
|
public ContentPatchLoadEvent(Seq<String> patches){
|
||||||
|
this.patches = patches;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static class SaveLoadEvent{
|
public static class SaveLoadEvent{
|
||||||
public final boolean isMap;
|
public final boolean isMap;
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ public class SaveIO{
|
|||||||
/** Save format header. */
|
/** Save format header. */
|
||||||
public static final byte[] header = {'M', 'S', 'A', 'V'};
|
public static final byte[] header = {'M', 'S', 'A', 'V'};
|
||||||
public static final IntMap<SaveVersion> versions = new IntMap<>();
|
public static final IntMap<SaveVersion> versions = new IntMap<>();
|
||||||
public static final Seq<SaveVersion> versionArray = Seq.with(new Save1(), new Save2(), new Save3(), new Save4(), new Save5(), new Save6(), new Save7(), new Save8(), new Save9(), new Save10());
|
public static final Seq<SaveVersion> versionArray = Seq.with(new Save1(), new Save2(), new Save3(), new Save4(), new Save5(), new Save6(), new Save7(), new Save8(), new Save9(), new Save10(), new Save11());
|
||||||
|
|
||||||
static{
|
static{
|
||||||
for(SaveVersion version : versionArray){
|
for(SaveVersion version : versionArray){
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import mindustry.core.*;
|
|||||||
import mindustry.ctype.*;
|
import mindustry.ctype.*;
|
||||||
import mindustry.entities.*;
|
import mindustry.entities.*;
|
||||||
import mindustry.game.*;
|
import mindustry.game.*;
|
||||||
|
import mindustry.game.EventType.*;
|
||||||
import mindustry.game.Teams.*;
|
import mindustry.game.Teams.*;
|
||||||
import mindustry.gen.*;
|
import mindustry.gen.*;
|
||||||
import mindustry.maps.Map;
|
import mindustry.maps.Map;
|
||||||
@@ -67,6 +68,7 @@ public abstract class SaveVersion extends SaveFileReader{
|
|||||||
readRegion("content", stream, counter, this::readContentHeader);
|
readRegion("content", stream, counter, this::readContentHeader);
|
||||||
|
|
||||||
try{
|
try{
|
||||||
|
if(version >= 11) readRegion("patches", stream, counter, this::readContentPatches);
|
||||||
readRegion("map", stream, counter, in -> readMap(in, context));
|
readRegion("map", stream, counter, in -> readMap(in, context));
|
||||||
readRegion("entities", stream, counter, this::readEntities);
|
readRegion("entities", stream, counter, this::readEntities);
|
||||||
if(version >= 8) readRegion("markers", stream, counter, this::readMarkers);
|
if(version >= 8) readRegion("markers", stream, counter, this::readMarkers);
|
||||||
@@ -79,6 +81,7 @@ public abstract class SaveVersion extends SaveFileReader{
|
|||||||
public void write(DataOutputStream stream, StringMap extraTags) throws IOException{
|
public void write(DataOutputStream stream, StringMap extraTags) throws IOException{
|
||||||
writeRegion("meta", stream, out -> writeMeta(out, extraTags));
|
writeRegion("meta", stream, out -> writeMeta(out, extraTags));
|
||||||
writeRegion("content", stream, this::writeContentHeader);
|
writeRegion("content", stream, this::writeContentHeader);
|
||||||
|
writeRegion("patches", stream, this::writeContentPatches);
|
||||||
writeRegion("map", stream, this::writeMap);
|
writeRegion("map", stream, this::writeMap);
|
||||||
writeRegion("entities", stream, this::writeEntities);
|
writeRegion("entities", stream, this::writeEntities);
|
||||||
writeRegion("markers", stream, this::writeMarkers);
|
writeRegion("markers", stream, this::writeMarkers);
|
||||||
@@ -502,8 +505,46 @@ public abstract class SaveVersion extends SaveFileReader{
|
|||||||
readWorldEntities(stream, mapping);
|
readWorldEntities(stream, mapping);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void readContentPatches(DataInput stream) throws IOException{
|
||||||
|
Seq<String> patches = new Seq<>();
|
||||||
|
|
||||||
|
int amount = stream.readUnsignedByte();
|
||||||
|
if(amount > 0){
|
||||||
|
for(int i = 0; i < amount; i++){
|
||||||
|
int len = stream.readInt();
|
||||||
|
byte[] bytes = new byte[len];
|
||||||
|
stream.readFully(bytes);
|
||||||
|
patches.add(new String(bytes, Strings.utf8));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Events.fire(new ContentPatchLoadEvent(patches));
|
||||||
|
|
||||||
|
if(patches.size > 0){
|
||||||
|
try{
|
||||||
|
state.patcher.apply(patches);
|
||||||
|
}catch(Throwable e){
|
||||||
|
Log.err("Failed to apply patches: " + patches, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void writeContentPatches(DataOutput stream) throws IOException{
|
||||||
|
if(state.patcher.patches.size > 0){
|
||||||
|
var patches = state.patcher.patches;
|
||||||
|
stream.writeByte(patches.size);
|
||||||
|
for(var patchset : patches){
|
||||||
|
byte[] bytes = patchset.patch.getBytes(Strings.utf8);
|
||||||
|
stream.writeInt(bytes.length);
|
||||||
|
stream.write(bytes);
|
||||||
|
}
|
||||||
|
}else{
|
||||||
|
stream.writeByte(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void readContentHeader(DataInput stream) throws IOException{
|
public void readContentHeader(DataInput stream) throws IOException{
|
||||||
byte mapped = stream.readByte();
|
int mapped = stream.readUnsignedByte();
|
||||||
|
|
||||||
MappableContent[][] map = new MappableContent[ContentType.all.length][0];
|
MappableContent[][] map = new MappableContent[ContentType.all.length][0];
|
||||||
|
|
||||||
@@ -520,6 +561,21 @@ public abstract class SaveVersion extends SaveFileReader{
|
|||||||
}
|
}
|
||||||
|
|
||||||
content.setTemporaryMapper(map);
|
content.setTemporaryMapper(map);
|
||||||
|
|
||||||
|
//HACK: versions below 11 don't read the patch chunk, which means the event for reading patches is never triggered.
|
||||||
|
//manually fire the event here for older versions.
|
||||||
|
if(version < 11){
|
||||||
|
Seq<String> patches = new Seq<>();
|
||||||
|
Events.fire(new ContentPatchLoadEvent(patches));
|
||||||
|
|
||||||
|
if(patches.size > 0){
|
||||||
|
try{
|
||||||
|
state.patcher.apply(patches);
|
||||||
|
}catch(Throwable e){
|
||||||
|
Log.err("Failed to apply patches: " + patches, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void writeContentHeader(DataOutput stream) throws IOException{
|
public void writeContentHeader(DataOutput stream) throws IOException{
|
||||||
|
|||||||
11
core/src/mindustry/io/versions/Save11.java
Normal file
11
core/src/mindustry/io/versions/Save11.java
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
package mindustry.io.versions;
|
||||||
|
|
||||||
|
import mindustry.io.*;
|
||||||
|
|
||||||
|
/** Adds patches in content header. */
|
||||||
|
public class Save11 extends SaveVersion{
|
||||||
|
|
||||||
|
public Save11(){
|
||||||
|
super(11);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import java.lang.reflect.*;
|
|||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
/** The current implementation is awful. Consider it a proof of concept. */
|
/** The current implementation is awful. Consider it a proof of concept. */
|
||||||
|
//TODO block consumer support
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public class ContentPatcher{
|
public class ContentPatcher{
|
||||||
private static final Object root = new Object();
|
private static final Object root = new Object();
|
||||||
@@ -25,6 +26,10 @@ public class ContentPatcher{
|
|||||||
private ObjectSet<PatchRecord> usedpatches = new ObjectSet<>();
|
private ObjectSet<PatchRecord> usedpatches = new ObjectSet<>();
|
||||||
private Seq<Runnable> resetters = new Seq<>();
|
private Seq<Runnable> resetters = new Seq<>();
|
||||||
private Seq<Runnable> afterCallbacks = new Seq<>();
|
private Seq<Runnable> afterCallbacks = new Seq<>();
|
||||||
|
private @Nullable PatchSet currentlyApplying;
|
||||||
|
|
||||||
|
/** Currently active patches. Note that apply() should be called after modification. */
|
||||||
|
public Seq<PatchSet> patches = new Seq<>();
|
||||||
|
|
||||||
static{
|
static{
|
||||||
for(var type : ContentType.all){
|
for(var type : ContentType.all){
|
||||||
@@ -32,22 +37,37 @@ public class ContentPatcher{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void apply(String patch) throws Exception{
|
/** Applies the specified patches. If patches were already applied, the previous ones are un-applied - they do not stack! */
|
||||||
|
public void apply(Seq<String> patchArray) throws Exception{
|
||||||
|
if(applied){
|
||||||
|
unapply();
|
||||||
|
applied = false;
|
||||||
|
}
|
||||||
json = Vars.mods.getContentParser().getJson();
|
json = Vars.mods.getContentParser().getJson();
|
||||||
|
|
||||||
applied = true;
|
applied = true;
|
||||||
contentLoader = Vars.content.copy();
|
contentLoader = Vars.content.copy();
|
||||||
|
patches.clear();
|
||||||
|
|
||||||
try{
|
for(String patch : patchArray){
|
||||||
JsonValue value = json.fromJson(null, Jval.read(patch).toString(Jformat.plain));
|
try{
|
||||||
for(var child : value){
|
JsonValue value = json.fromJson(null, Jval.read(patch).toString(Jformat.plain));
|
||||||
assign(root, child.name, child, null, null, null);
|
PatchSet set = new PatchSet(patch, value);
|
||||||
|
patches.add(set);
|
||||||
|
currentlyApplying = set;
|
||||||
|
|
||||||
|
value.remove("name"); //patchsets can have a name, ignore it if present
|
||||||
|
for(var child : value){
|
||||||
|
assign(root, child.name, child, null, null, null);
|
||||||
|
}
|
||||||
|
currentlyApplying = null;
|
||||||
|
|
||||||
|
}catch(Exception e){
|
||||||
|
Log.err("Failed to apply patch: " + patch, e);
|
||||||
}
|
}
|
||||||
|
|
||||||
afterCallbacks.each(Runnable::run);
|
|
||||||
}catch(Exception e){
|
|
||||||
Log.err("Failed to apply patch: " + patch, e);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
afterCallbacks.each(Runnable::run);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void unapply(){
|
public void unapply(){
|
||||||
@@ -69,6 +89,7 @@ public class ContentPatcher{
|
|||||||
//this should never throw an exception
|
//this should never throw an exception
|
||||||
afterCallbacks.each(Runnable::run);
|
afterCallbacks.each(Runnable::run);
|
||||||
afterCallbacks.clear();
|
afterCallbacks.clear();
|
||||||
|
usedpatches.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
void assign(Object object, String field, Object value, @Nullable FieldData metadata, @Nullable Object parentObject, @Nullable String parentField) throws Exception{
|
void assign(Object object, String field, Object value, @Nullable FieldData metadata, @Nullable Object parentObject, @Nullable String parentField) throws Exception{
|
||||||
@@ -168,6 +189,10 @@ public class ContentPatcher{
|
|||||||
assignValue(object, field, metadata, () -> Array.get(fobj, i), val -> Array.set(fobj, i, val), value, false);
|
assignValue(object, field, metadata, () -> Array.get(fobj, i), val -> Array.set(fobj, i, val), value, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}else if(object instanceof ObjectSet set && prefix == '+'){
|
||||||
|
modifiedField(parentObject, parentField, set.copy());
|
||||||
|
|
||||||
|
assignValue(object, field, metadata, () -> null, val -> set.add(val), value, false);
|
||||||
}else if(object instanceof ObjectMap map){
|
}else if(object instanceof ObjectMap map){
|
||||||
if(metadata == null){
|
if(metadata == null){
|
||||||
warn("ObjectMap cannot be parsed without metadata: @.@", parentObject, parentField);
|
warn("ObjectMap cannot be parsed without metadata: @.@", parentObject, parentField);
|
||||||
@@ -182,7 +207,13 @@ public class ContentPatcher{
|
|||||||
var copy = map.copy();
|
var copy = map.copy();
|
||||||
reset(() -> map.set(copy));
|
reset(() -> map.set(copy));
|
||||||
|
|
||||||
assignValue(object, field, new FieldData(metadata.elementType, null, null), () -> map.get(key), val -> map.put(key, val), value, false);
|
if(value instanceof JsonValue jval && jval.isString() && (jval.asString().equals("-"))){
|
||||||
|
//removal syntax:
|
||||||
|
//"value": "-"
|
||||||
|
map.remove(key);
|
||||||
|
}else{
|
||||||
|
assignValue(object, field, new FieldData(metadata.elementType, null, null), () -> map.get(key), val -> map.put(key, val), value, false);
|
||||||
|
}
|
||||||
}else{
|
}else{
|
||||||
Class<?> actualType = object.getClass();
|
Class<?> actualType = object.getClass();
|
||||||
if(actualType.isAnonymousClass()) actualType = actualType.getSuperclass();
|
if(actualType.isAnonymousClass()) actualType = actualType.getSuperclass();
|
||||||
@@ -193,9 +224,15 @@ public class ContentPatcher{
|
|||||||
if(checkField(fdata.field)) return;
|
if(checkField(fdata.field)) return;
|
||||||
|
|
||||||
var fobj = object;
|
var fobj = object;
|
||||||
assignValue(object, field, new FieldData(fdata), () -> Reflect.get(fobj, fdata.field), fv -> Reflect.set(fobj, fdata.field, fv), value, true);
|
assignValue(object, field, new FieldData(fdata), () -> Reflect.get(fobj, fdata.field), fv -> {
|
||||||
|
if(fv == null && !fdata.field.isAnnotationPresent(Nullable.class)){
|
||||||
|
warn("Field '@' cannot be null.", fdata.field);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Reflect.set(fobj, fdata.field, fv);
|
||||||
|
}, value, true);
|
||||||
}else{
|
}else{
|
||||||
warn("Unknown field: '@' for '@'", field, actualType.getName());
|
warn("Unknown field: '@' for class '@'", field, actualType.getSimpleName());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -322,9 +359,12 @@ public class ContentPatcher{
|
|||||||
return json.fromJson(type, string);
|
return json.fromJson(type, string);
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO crash?
|
|
||||||
void warn(String error, Object... fmt){
|
void warn(String error, Object... fmt){
|
||||||
Log.warn(error, fmt);
|
String formatted = Strings.format(error, fmt);
|
||||||
|
if(currentlyApplying != null){
|
||||||
|
currentlyApplying.warnings.add(formatted);
|
||||||
|
}
|
||||||
|
Log.warn("[ContentPatcher] " + formatted);
|
||||||
}
|
}
|
||||||
|
|
||||||
void after(Runnable run){
|
void after(Runnable run){
|
||||||
@@ -343,6 +383,19 @@ public class ContentPatcher{
|
|||||||
return ((Object[])object).clone();
|
return ((Object[])object).clone();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static class PatchSet{
|
||||||
|
public String patch;
|
||||||
|
public JsonValue json;
|
||||||
|
public String name;
|
||||||
|
public Seq<String> warnings = new Seq<>();
|
||||||
|
|
||||||
|
public PatchSet(String patch, JsonValue json){
|
||||||
|
this.patch = patch;
|
||||||
|
this.json = json;
|
||||||
|
name = json.getString("name", "");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static class FieldData{
|
private static class FieldData{
|
||||||
Class type, elementType, keyType;
|
Class type, elementType, keyType;
|
||||||
|
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ public class NetworkIO{
|
|||||||
player.write(new Writes(stream));
|
player.write(new Writes(stream));
|
||||||
|
|
||||||
SaveIO.getSaveWriter().writeContentHeader(stream);
|
SaveIO.getSaveWriter().writeContentHeader(stream);
|
||||||
|
SaveIO.getSaveWriter().writeContentPatches(stream);
|
||||||
SaveIO.getSaveWriter().writeMap(stream);
|
SaveIO.getSaveWriter().writeMap(stream);
|
||||||
SaveIO.getSaveWriter().writeTeamBlocks(stream);
|
SaveIO.getSaveWriter().writeTeamBlocks(stream);
|
||||||
SaveIO.getSaveWriter().writeMarkers(stream);
|
SaveIO.getSaveWriter().writeMarkers(stream);
|
||||||
@@ -84,6 +85,7 @@ public class NetworkIO{
|
|||||||
player.add();
|
player.add();
|
||||||
|
|
||||||
SaveIO.getSaveWriter().readContentHeader(stream);
|
SaveIO.getSaveWriter().readContentHeader(stream);
|
||||||
|
SaveIO.getSaveWriter().readContentPatches(stream);
|
||||||
SaveIO.getSaveWriter().readMap(stream, world.context);
|
SaveIO.getSaveWriter().readMap(stream, world.context);
|
||||||
SaveIO.getSaveWriter().readTeamBlocks(stream);
|
SaveIO.getSaveWriter().readTeamBlocks(stream);
|
||||||
SaveIO.getSaveWriter().readMarkers(stream);
|
SaveIO.getSaveWriter().readMarkers(stream);
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ public class Fonts{
|
|||||||
private static ObjectMap<String, String> stringIcons = new ObjectMap<>();
|
private static ObjectMap<String, String> stringIcons = new ObjectMap<>();
|
||||||
private static ObjectMap<String, TextureRegion> largeIcons = new ObjectMap<>();
|
private static ObjectMap<String, TextureRegion> largeIcons = new ObjectMap<>();
|
||||||
|
|
||||||
public static Font def, outline, icon, iconLarge, tech, logic;
|
public static Font def, outline, icon, iconLarge, tech, logic, monospace;
|
||||||
|
|
||||||
public static int getUnicode(String content){
|
public static int getUnicode(String content){
|
||||||
return unicodeIcons.get(content, 0);
|
return unicodeIcons.get(content, 0);
|
||||||
@@ -66,6 +66,13 @@ public class Fonts{
|
|||||||
|
|
||||||
Core.assets.load("default", Font.class, new FreeTypeFontLoaderParameter(mainFont, param)).loaded = f -> Fonts.def = f;
|
Core.assets.load("default", Font.class, new FreeTypeFontLoaderParameter(mainFont, param)).loaded = f -> Fonts.def = f;
|
||||||
|
|
||||||
|
Core.assets.load("monospace", Font.class, new FreeTypeFontLoaderParameter("fonts/monospace.woff", new FreeTypeFontParameter(){{
|
||||||
|
size = 16;
|
||||||
|
incremental = true;
|
||||||
|
//most people will never see the monospace font, so don't pre-bake anything
|
||||||
|
characters = "\u0000 ";
|
||||||
|
}})).loaded = f -> Fonts.monospace = f;
|
||||||
|
|
||||||
Core.assets.load("icon", Font.class, new FreeTypeFontLoaderParameter("fonts/icon.ttf", new FreeTypeFontParameter(){{
|
Core.assets.load("icon", Font.class, new FreeTypeFontLoaderParameter("fonts/icon.ttf", new FreeTypeFontParameter(){{
|
||||||
size = 30;
|
size = 30;
|
||||||
incremental = true;
|
incremental = true;
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ public class Styles{
|
|||||||
|
|
||||||
public static ScrollPaneStyle defaultPane, horizontalPane, smallPane, noBarPane;
|
public static ScrollPaneStyle defaultPane, horizontalPane, smallPane, noBarPane;
|
||||||
public static SliderStyle defaultSlider;
|
public static SliderStyle defaultSlider;
|
||||||
public static LabelStyle defaultLabel, outlineLabel, techLabel;
|
public static LabelStyle defaultLabel, outlineLabel, techLabel, monoLabel;
|
||||||
public static TextFieldStyle defaultField, nodeField, areaField, nodeArea;
|
public static TextFieldStyle defaultField, nodeField, areaField, nodeArea;
|
||||||
public static CheckBoxStyle defaultCheck;
|
public static CheckBoxStyle defaultCheck;
|
||||||
public static DialogStyle defaultDialog, fullDialog;
|
public static DialogStyle defaultDialog, fullDialog;
|
||||||
@@ -380,6 +380,10 @@ public class Styles{
|
|||||||
font = Fonts.tech;
|
font = Fonts.tech;
|
||||||
fontColor = Color.white;
|
fontColor = Color.white;
|
||||||
}};
|
}};
|
||||||
|
monoLabel = new LabelStyle(){{
|
||||||
|
font = Fonts.monospace;
|
||||||
|
fontColor = Color.white;
|
||||||
|
}};
|
||||||
|
|
||||||
defaultField = new TextFieldStyle(){{
|
defaultField = new TextFieldStyle(){{
|
||||||
font = Fonts.def;
|
font = Fonts.def;
|
||||||
|
|||||||
@@ -26,4 +26,4 @@ org.gradle.caching=true
|
|||||||
org.gradle.internal.http.socketTimeout=100000
|
org.gradle.internal.http.socketTimeout=100000
|
||||||
org.gradle.internal.http.connectionTimeout=100000
|
org.gradle.internal.http.connectionTimeout=100000
|
||||||
android.enableR8.fullMode=false
|
android.enableR8.fullMode=false
|
||||||
archash=7a3d906e1b
|
archash=c8f3bd901b
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import arc.util.CommandHandler.*;
|
|||||||
import arc.util.Timer.*;
|
import arc.util.Timer.*;
|
||||||
import arc.util.serialization.*;
|
import arc.util.serialization.*;
|
||||||
import arc.util.serialization.JsonValue.*;
|
import arc.util.serialization.JsonValue.*;
|
||||||
|
import arc.util.serialization.Jval.*;
|
||||||
import mindustry.*;
|
import mindustry.*;
|
||||||
import mindustry.core.GameState.*;
|
import mindustry.core.GameState.*;
|
||||||
import mindustry.core.*;
|
import mindustry.core.*;
|
||||||
@@ -72,6 +73,8 @@ public class ServerControl implements ApplicationListener{
|
|||||||
private PrintWriter socketOutput;
|
private PrintWriter socketOutput;
|
||||||
private String suggested;
|
private String suggested;
|
||||||
private boolean autoPaused = false;
|
private boolean autoPaused = false;
|
||||||
|
private Fi patchDirectory;
|
||||||
|
private Seq<String> contentPatches = new Seq<>();
|
||||||
|
|
||||||
public Cons<GameOverEvent> gameOverListener = event -> {
|
public Cons<GameOverEvent> gameOverListener = event -> {
|
||||||
if(state.rules.waves){
|
if(state.rules.waves){
|
||||||
@@ -191,13 +194,17 @@ public class ServerControl implements ApplicationListener{
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
customMapDirectory.mkdirs();
|
|
||||||
|
|
||||||
if(Version.build == -1){
|
if(Version.build == -1){
|
||||||
warn("&lyYour server is running a custom build, which means that client checking is disabled.");
|
warn("&lyYour server is running a custom build, which means that client checking is disabled.");
|
||||||
warn("&lyIt is highly advised to specify which version you're using by building with gradle args &lb&fb-Pbuildversion=&lr<build>");
|
warn("&lyIt is highly advised to specify which version you're using by building with gradle args &lb&fb-Pbuildversion=&lr<build>");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customMapDirectory.mkdirs();
|
||||||
|
|
||||||
|
patchDirectory = dataDirectory.child("patches");
|
||||||
|
patchDirectory.mkdirs();
|
||||||
|
loadPatchFiles();
|
||||||
|
|
||||||
//set up default shuffle mode
|
//set up default shuffle mode
|
||||||
try{
|
try{
|
||||||
maps.setShuffleMode(ShuffleMode.valueOf(Core.settings.getString("shufflemode")));
|
maps.setShuffleMode(ShuffleMode.valueOf(Core.settings.getString("shufflemode")));
|
||||||
@@ -314,6 +321,30 @@ public class ServerControl implements ApplicationListener{
|
|||||||
|
|
||||||
info("Server loaded. Type @ for help.", "'help'");
|
info("Server loaded. Type @ for help.", "'help'");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Events.on(ContentPatchLoadEvent.class, event -> {
|
||||||
|
//NOTE: if patches change, and an older save is loaded, the patches will be applied twice; the old ones won't be removed.
|
||||||
|
for(String patch : contentPatches){
|
||||||
|
event.patches.addUnique(patch);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void loadPatchFiles(){
|
||||||
|
contentPatches.clear();
|
||||||
|
Seq<Fi> patches = patchDirectory.findAll(f -> f.extEquals("json") || f.extEquals("hjson") || f.extEquals("json5")).sort();
|
||||||
|
|
||||||
|
for(Fi patch : patches){
|
||||||
|
try{
|
||||||
|
contentPatches.add(Jval.read(patch.readString()).toString(Jformat.plain));
|
||||||
|
}catch(Throwable e){
|
||||||
|
Log.err("Invalid patch file: " + patch.name(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if(contentPatches.size > 0){
|
||||||
|
Log.info("Loaded @ content patch files.", contentPatches.size);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void registerCommands(){
|
protected void registerCommands(){
|
||||||
|
|||||||
Reference in New Issue
Block a user