Compare commits

...
12 Commits
Author SHA1 Message Date
BalaM314 beaa1da276 Allow closing the console after you've disabled it (#12737)
* Allow closing the console after you've disabled it

* Close the console immediately when disabled
2026-10-03 12:35:04 -04:00
Anuken a378c35fb6 Contribution guidelines & AI policy update 2026-10-03 11:07:11 -04:00
Anuken 69ed90bb9d Directional unloader read fix 2026-09-30 18:02:13 -04:00
Anuken e337452d46 Reverted #12722 on master branch (will be back on v9)
This reverts commit 9f70386466.
2026-09-30 17:59:01 -04:00
Twcash 9f70386466 fix Copper ore color to match item color (#12722) 2026-09-30 17:56:38 -04:00
Cardillan e7de4e1a10 Fix empty statement fields shifting the remaining parameters (#12715)
* Fix empty statement fields shifting remaining parameters

* Fixed unit tests
2026-09-30 17:55:18 -04:00
WayZer 4cc15b84f6 Do not let content JSON set Effect#initialized (#12724) 2026-09-29 14:29:43 -04:00
Anuken f3bfa418a5 Fixed rare physics crash 2026-09-27 18:45:23 -04:00
Anuken 663b0b8737 Backported patch loading in mods from v9 for future compatibility 2026-09-25 16:47:42 -04:00
Anuken 3a54813513 Fixed some schema generation bugs 2026-09-20 18:40:01 -04:00
Anuken 3e43d88374 WIP re-generation of regions on server 2026-09-20 17:27:51 -04:00
Anuken 580f8f5217 Fixed #12697 2026-09-20 13:55:06 -04:00
16 changed files with 103 additions and 37 deletions
+2 -3
View File
@@ -1,6 +1,5 @@
If your pull request is **not** translation-related, read the list of requirements below and check each box:
- [ ] I have read the [contribution guidelines](https://github.com/Anuken/Mindustry/blob/master/CONTRIBUTING.md).
- [ ] I have ensured that my code compiles, if applicable.
- [ ] I have ensured that any new features in this PR function correctly in-game, if applicable.
- [ ] I affirm that this code was not written using generative AI/LLMs.
- [ ] I have ensured that any new features or fixes in this PR function correctly in-game, if applicable.
- [ ] I affirm that this code follows Mindustry's [AI policy](https://github.com/Anuken/Mindustry/blob/master/CONTRIBUTING.md#ai-policy).
+17 -7
View File
@@ -4,9 +4,6 @@ This is for code contributions. For translations, see [TRANSLATING](TRANSLATING.
## Basic Guidelines
### Use an IDE.
Specifically, IntelliJ IDEA. Download the (free) Community Edition of it [here](https://www.jetbrains.com/idea/download/). Some people use other tools, like VS Code, but I would personally not recommend them for Java development.
### Always test your changes.
Do not submit something without at least running the game to see if it compiles.
If you are submitting a new block, make sure it has a name and description, and that it works correctly in-game. If you are changing existing block mechanics, test them out first.
@@ -20,11 +17,19 @@ Yes, there are occurrences of trailing spaces, extra newlines, empty indents, an
I **especially** do not want to see PRs that apply any kind of automated analysis to the source code to "optimize" anything - my IDE can do that already. If the PR doesn't actually change anything useful, I'm not going to review or merge it.
### Do not make AI "contributions".
## AI Policy
If I see a PR with code that's obviously written by AI, I will reject your PR, and you will be blocked. Don't waste my time with slop.
Mindustry's AI contribution policies are similar to that of [the Rust language](https://forge.rust-lang.org/policies/llm-usage.html), which are summarized as follows:
Asking AI questions, and using that information to help you write code? Fine. Using it to actually write code? No.
- ✔️ You **are** allowed use AI to review your own code, summarize, inform, and answer questions.
- ✔️ You **are** allowed to use AI to translate your *human-written* messages into English, if necessary.
- ❌ You are **not** allowed to submit PRs that are generated by AI.
- ❌ You are **not** allowed to use AI to *generate* any text posted (PRs, issues, commit messages, etc.)
- ⚠️ If you post *anything* that is based on AI output (potential solution to a bug, crash diagnosis...), it must be clearly marked as being sourced from AI.
Repeated (or particularly egregious) violations of these rules will get you banned from this repository.
## Style Guidelines
@@ -80,4 +85,9 @@ If something needs to be encapsulated in the future, IntelliJ can handle it with
Unless a block of code is very large or used in more than 1-2 places, don't split it up into a separate method. Making unnecessary methods only creates confusion, and may slightly decrease performance.
## Other Notes
If you would like your name to appear in the game's credits, add it to the [list of contributors](https://github.com/Anuken/Mindustry/blob/master/core/assets/contributors) as part of your PR.
- If you would like your name to appear in the game's credits, add it to the [list of contributors](https://github.com/Anuken/Mindustry/blob/master/core/assets/contributors) as part of your PR.
- Using an IDE is highly recommended, but not required.
- I use IntelliJ; download it [here](https://www.jetbrains.com/idea/download/).
- Some people use other tools, like VS Code, but I haven't used them for Java myself, and cannot guarantee that they will work well for this codebase.
+4 -1
View File
@@ -183,7 +183,10 @@ public abstract class ClientLauncher extends ApplicationCore implements Platform
assets.load(schematics);
assets.loadRun("contentinit", ContentLoader.class, () -> content.init(), () -> content.load());
assets.loadRun("contentinit", ContentLoader.class, () -> content.init(), () -> {
content.load();
mods.loadModPatches();
});
assets.loadRun("baseparts", BaseRegistry.class, () -> {}, () -> bases.load());
Core.assets.load("sprites/schematic-background.png", Texture.class).loaded = t -> t.setWrap(TextureWrap.repeat);
+1 -2
View File
@@ -8,7 +8,6 @@ import arc.struct.*;
import arc.util.TaskQueue;
import arc.util.*;
import mindustry.annotations.Annotations.*;
import mindustry.content.*;
import mindustry.core.*;
import mindustry.game.EventType.*;
import mindustry.game.*;
@@ -263,7 +262,7 @@ public class Pathfinder implements Runnable{
tile.build == null || !solid || tile.block() instanceof CoreBlock ? 0 : Math.min((int)(tile.build.health / 40), 80),
tid == 0 && tile.build != null && state.rules.coreCapture ? 255 : tid, //use teamid = 255 when core capture is enabled to mark out derelict structures
solid,
tile.floor().isLiquid && tile.block() == Blocks.air,
tile.floor().isLiquid && !tile.block().solid,
tile.legSolid(),
nearLiquid,
nearGround,
+10 -1
View File
@@ -26,6 +26,7 @@ public class PhysicsProcess implements AsyncProcess{
private PhysicsWorld[] physics;
private Seq<PhysicRef> refs = new Seq<>(false, 20, PhysicRef.class);
private Seq<Future<?>> futures = new Seq<>(false, layers, Future.class);
private Seq<PhysicRef> pendingAdds = new Seq<>(false, 20, PhysicRef.class);
private static volatile long maxPhysicsTime = 0;
@@ -44,7 +45,8 @@ public class PhysicsProcess implements AsyncProcess{
PhysicRef ref = new PhysicRef(unit, body);
refs.add(ref);
if(ref.lastLayer >= 0) physics[ref.lastLayer].add(body);
//defer adds until physics update is done
pendingAdds.add(ref);
}
@Override
@@ -68,6 +70,13 @@ public class PhysicsProcess implements AsyncProcess{
PerfCounter.unitPhysicsAsync.add(maxPhysicsTime);
maxPhysicsTime = 0;
//flush newly added entities
for(int i = 0; i < pendingAdds.size; i++){
PhysicRef ref = pendingAdds.items[i];
if(ref.lastLayer >= 0) physics[ref.lastLayer].add(ref.body);
}
pendingAdds.clear();
//move entities
for(PhysicRef ref : refs){
Physicsc entity = ref.entity;
+1 -1
View File
@@ -23,7 +23,7 @@ public class Effect{
public static final Seq<Effect> all = new Seq<>();
private boolean initialized;
private transient boolean initialized;
public final int id;
+1 -1
View File
@@ -90,7 +90,7 @@ public abstract class LStatement{
/** Sanitizes variable input strings from text fields into values that will not break logic parsing. */
public static String sanitize(String value){
if(value.length() == 0){
return "";
return "null";
}else if(value.length() == 1){
if(value.charAt(0) == '"' || value.charAt(0) == ';' || value.charAt(0) == ' ' ||
value.charAt(0) == '\n' || value.charAt(0) == '\t' || value.charAt(0) == '#'){
+18 -12
View File
@@ -78,6 +78,7 @@ public class DataManager{
PixmapRegion error = new PixmapRegion(Pixmaps.blankPixmap());
UnlockableContent[] currentContent = {null};
String[] currentHash = {null};
Fi serverGeneratedDir = Vars.dataDirectory.child("assets/sprites/generated");
MultiPacker saver = new MultiPacker(false){
@Override
@@ -89,10 +90,17 @@ public class DataManager{
if(name.startsWith(DataImagePacker.regionPrefix)) name = name.substring(DataImagePacker.regionPrefix.length());
String path = "generated/" + currentHash[0] + "/" + name + ".png";
ImageAsset newImage = new ImageAsset();
newImage.setPath(path);
//it would be nice to do this async, but the pixmap typically gets disposed right after add() exist
//it would be nice to do this async, but the pixmap typically gets disposed right after add()
byte[] bytes = PixmapIO.writePngBytes(region.pixmap);
newImage.updateData(bytes);
if(Vars.headless){ //TODO: doesn't actually work on the server yet
Fi file = serverGeneratedDir.child(name + ".png");
file.writeBytes(bytes);
newImage.readOverride(path, file);
}else{
newImage.setPath(path);
newImage.updateData(bytes);
}
images.add(newImage);
packed[0] ++;
@@ -150,17 +158,15 @@ public class DataManager{
imagePixmaps.each((key, val) -> val.pixmap.dispose());
error.pixmap.dispose();
if(packed[0] > 0 || forcePack){
if(!Vars.headless && (packed[0] > 0 || forcePack)){
reloadImages();
if(!Vars.headless){
for(var cont : contentToPack){
try{
cont.loadIcon();
cont.load();
}catch(Exception e){
Log.err("Failed to load icons for " + cont, e);
}
for(var cont : contentToPack){
try{
cont.loadIcon();
cont.load();
}catch(Exception e){
Log.err("Failed to load icons for " + cont, e);
}
}
}
+19
View File
@@ -22,6 +22,7 @@ import mindustry.gen.*;
import mindustry.graphics.*;
import mindustry.graphics.MultiPacker.*;
import mindustry.mod.ContentParser.*;
import mindustry.mod.data.*;
import mindustry.type.*;
import mindustry.ui.*;
@@ -936,6 +937,24 @@ public class Mods implements Loadable{
Events.fire(new ModContentLoadEvent());
}
public void loadModPatches(){
for(LoadedMod mod : orderedMods()){
if(mod.root.child("patches").exists()){
try{
Seq<Fi> patches = mod.root.child("patches").findAll(f -> f.extEquals("json") || f.extEquals("hjson"));
if(patches.size > 0){
//create a new patcher per mod so the fields don't get reset
DataPatcher patcher = new DataPatcher();
patches.sort();
patcher.apply(patches.map(f -> new PatchAsset(f.readString())), new Seq<>());
}
}catch(Exception e){
Log.err("Failed to apply patches from mod " + mod.meta.name, e);
}
}
}
}
public void handleContentError(Content content, Throwable error){
parser.markError(content, error);
}
@@ -563,7 +563,11 @@ public class SettingsMenuDialog extends BaseDialog{
Core.settings.put("swapdiagonal", false);
}
dev.checkPref("console", false);
dev.checkPref("console", false, val -> {
if(val == false && ui.consolefrag != null){
ui.consolefrag.close();
}
});
dev.checkPref("drawhitboxes", false);
dev.checkPref("showperformance", false);
@@ -40,7 +40,7 @@ public class ConsoleFragment extends Table{
font = Fonts.def;
visible(() -> {
if(input.keyTap(Binding.console) && settings.getBool("console") && (scene.getKeyboardFocus() == chatfield || !(scene.getKeyboardFocus() instanceof TextField)) && !ui.chatfrag.shown()){
if(input.keyTap(Binding.console) && (settings.getBool("console") || shown) && (scene.getKeyboardFocus() == chatfield || !(scene.getKeyboardFocus() instanceof TextField)) && !ui.chatfrag.shown()){
shown = !shown;
if(shown && !open && settings.getBool("console")){
toggle();
@@ -227,6 +227,11 @@ public class ConsoleFragment extends Table{
open = false; //never true on mobile
}
public void close(){
shown = false;
open = false;
}
public void toggle(){
if(!open){
@@ -162,7 +162,7 @@ public class DirectionalUnloader extends Block{
public void read(Reads read, byte revision){
super.read(read, revision);
int id = read.s();
unloadItem = id == -1 ? null : content.items().get(id);
unloadItem = id == -1 ? null : content.item(id);
offset = read.s();
}
}
+1 -1
View File
@@ -26,4 +26,4 @@ org.gradle.caching=true
org.gradle.internal.http.socketTimeout=100000
org.gradle.internal.http.connectionTimeout=100000
android.enableR8.fullMode=false
archash=8eb00ffff0
archash=7445105cd2
@@ -54,6 +54,7 @@ public class ServerLauncher implements ApplicationListener{
mods.loadScripts();
content.createModContent();
content.init();
mods.loadModPatches();
if(mods.hasContentErrors()){
err("Error occurred loading mod content:");
+2 -2
View File
@@ -123,7 +123,7 @@ public class LogicTests{
Arguments.of("a bare single semicolon is invalid", ";", "invalid"),
Arguments.of("a bare single space is invalid", " ", "invalid"),
Arguments.of("a single ordinary character passes through unchanged", "a", "a"),
Arguments.of("empty input stays empty", "", ""),
Arguments.of("empty input gets converted to null", "", "null"),
Arguments.of("a plain already-quoted value is untouched", "\"hello\"", "\"hello\""),
Arguments.of("a unescaped quote at the end gets doubled", "\"hello\\\"", "\"hello\\\\\""),
Arguments.of(
@@ -454,4 +454,4 @@ public class LogicTests{
void crlfLabelsResolveToTheSameJumpLocationAsLfLabels(){
assertDoesNotThrow(() -> load("loop:\r\njump loop always\r\n"));
}
}
}
+14 -3
View File
@@ -95,7 +95,12 @@ public class SchemaGenerator{
var root = getTypeDecl(field.getDeclaringClass());
if(root != null) root.getFieldByName(field.getName()).ifPresent(fdec -> {
String[] docAndDefault = determineJavadocAndDefault(field, fdec, fdec.getVariables().getFirst().orElseThrow());
var variable = fdec.getVariables().stream()
.filter(v -> v.getNameAsString().equals(field.getName()))
.findFirst().orElse(null);
if(variable == null) return;
String[] docAndDefault = determineJavadocAndDefault(field, fdec, variable);
if(docAndDefault[0] != null) inner.put("doc", docAndDefault[0]);
if(docAndDefault[1] != null) inner.put("default", docAndDefault[1]);
});
@@ -302,8 +307,14 @@ public class SchemaGenerator{
initValue = "[]";
}
//field
if(initValue.contains(".") && !(baseField.getType().isArray())){
//numeric literal: drop the Java-only suffix (f/F/d/D/L), keep the decimal point for JSON
boolean numeric = initValue.matches("-?(\\d+\\.?\\d*|\\.\\d+)([eE][-+]?\\d+)?[fFdDlL]?");
if(numeric){
initValue = initValue.replaceAll("[fFdDlL]$", "");
}
//field reference, e.g. Category.turret -> turret (never applied to numbers)
if(!numeric && initValue.contains(".") && !(baseField.getType().isArray())){
var split = initValue.split("\\.");
initValue = split[split.length - 1];
}