Weiter gearbeitet

This commit is contained in:
2026-06-04 22:40:17 +02:00
parent 875c39ab27
commit d56f2ea41f
108 changed files with 4283 additions and 1122 deletions

View File

@@ -0,0 +1,4 @@
package de.blight.common;
/** Einzeln platzierter Gras-Büschel. Y-Position wird zur Renderzeit aus dem Terrain berechnet. */
public record GrassTuft(float x, float z, float height, int slot) {}

View File

@@ -0,0 +1,86 @@
package de.blight.common;
import java.io.*;
import java.nio.file.*;
import java.util.*;
import java.util.zip.*;
/**
* Liest und schreibt individuell platzierte Gras-Büschel als komprimierte Binärdatei
* ({@code blight_grass.blg}) neben der Kartendatei.
*
* Format v1:
* int MAGIC 0x47525A53 ("GRZS")
* int VERSION 1
* int slotCount (8)
* 8× UTF Texturpfad pro Slot (Slot 0 = Standardtextur)
* int tuftCount
* N× float x, float z, float height, byte slot
*/
public final class GrassTuftIO {
private static final int MAGIC = 0x47525A53;
private static final int VERSION = 1;
private static final int SLOT_COUNT = 8;
private GrassTuftIO() {}
public static Path getPath() {
return MapIO.getMapPath().resolveSibling("blight_grass.blg");
}
public record GrassData(String[] slotPaths, List<GrassTuft> tufts) {}
public static void save(GrassData data) throws IOException {
Path p = getPath();
Files.createDirectories(p.getParent());
try (DataOutputStream out = new DataOutputStream(
new BufferedOutputStream(new GZIPOutputStream(Files.newOutputStream(p))))) {
out.writeInt(MAGIC);
out.writeInt(VERSION);
String[] paths = data.slotPaths() != null ? data.slotPaths() : new String[0];
out.writeInt(SLOT_COUNT);
for (int i = 0; i < SLOT_COUNT; i++) {
String s = (i < paths.length && paths[i] != null) ? paths[i] : "";
out.writeUTF(s);
}
List<GrassTuft> tufts = data.tufts() != null ? data.tufts() : List.of();
out.writeInt(tufts.size());
for (GrassTuft t : tufts) {
out.writeFloat(t.x());
out.writeFloat(t.z());
out.writeFloat(t.height());
out.writeByte(t.slot());
}
}
}
public static GrassData load() throws IOException {
Path p = getPath();
if (!Files.exists(p)) return null;
try (DataInputStream in = new DataInputStream(
new BufferedInputStream(new GZIPInputStream(Files.newInputStream(p))))) {
int magic = in.readInt();
int version = in.readInt();
if (magic != MAGIC) throw new IOException("Ungültige Grasdatei (Magic)");
if (version > VERSION) throw new IOException("Unbekannte Gras-Version: " + version);
int slotCount = in.readInt();
String[] paths = new String[SLOT_COUNT];
Arrays.fill(paths, "");
for (int i = 0; i < slotCount; i++) {
String s = in.readUTF();
if (i < SLOT_COUNT) paths[i] = s;
}
int tuftCount = in.readInt();
List<GrassTuft> tufts = new ArrayList<>(tuftCount);
for (int i = 0; i < tuftCount; i++) {
float x = in.readFloat();
float z = in.readFloat();
float h = in.readFloat();
int slot = in.readUnsignedByte();
tufts.add(new GrassTuft(x, z, h, slot));
}
return new GrassData(paths, tufts);
}
}
}

View File

@@ -70,6 +70,25 @@ public final class MapData {
/** Gras-Dichte [SPLAT_SIZE²], Bytes 0255 (0=kein Gras, 255=max Dichte). */
public final byte[] grassDensity;
/**
* Gras-Höhe pro Pixel [SPLAT_SIZE²].
* 0 = nicht gesetzt → grassDefaultHeight verwenden.
* 1255 = 0.1 m bis 10.0 m (linear: h = 0.1 + (b-1) * 9.9/254).
*/
public final byte[] grassHeightMap;
/** Relativer Asset-Pfad der Gras-Textur (z. B. "Textures/grass.png"), "" = Standardfarbe. */
public String grassTexturePath = "";
/** Standard-Blatt-Höhe für den Gras-Renderer (entspricht dem Grashöhe-Slider). */
public float grassDefaultHeight = 1.5f;
/** Per-Pixel Textur-Slot-Index [SPLAT_SIZE²]. 0=grassTexturePath, 1N=grassTextureSlots[slot-1]. */
public final byte[] grassTextureMap;
/** Texturpfade für Gras-Slots 1..N. Leerstring = nicht belegt. */
public String[] grassTextureSlots = new String[0];
/** Loch-Maske der oberen Schicht [UPPER_CELLS²], != 0 = Loch (Zelle ausgeblendet). */
public final byte[] upperHole;
@@ -91,7 +110,9 @@ public final class MapData {
upperSplatG = new byte [SPLAT_SIZE * SPLAT_SIZE];
upperSplatB = new byte [SPLAT_SIZE * SPLAT_SIZE];
upperSplatA = new byte [SPLAT_SIZE * SPLAT_SIZE];
grassDensity = new byte [SPLAT_SIZE * SPLAT_SIZE];
upperHole = new byte [UPPER_CELLS * UPPER_CELLS];
grassDensity = new byte [SPLAT_SIZE * SPLAT_SIZE];
grassHeightMap = new byte [SPLAT_SIZE * SPLAT_SIZE];
grassTextureMap = new byte [SPLAT_SIZE * SPLAT_SIZE];
upperHole = new byte [UPPER_CELLS * UPPER_CELLS];
}
}

View File

@@ -19,6 +19,9 @@ import java.util.zip.*;
* 4 wie 3 + Spawnpunkt (2× float)
* 5 wie 4 + Splatmap-Alpha + Texturpfade + Gebirge-Splatmap (RGBA + Pfade)
* 6 wie 5 ohne upperHole; upperTop/Bottom behalten (zukünftige Höhlen-Architektur)
* 7 wie 6 + Gras-Texturpfad (UTF-String) + Gras-Standardhöhe (float)
* 8 wie 7 + Gras-Höhen-Map (SPLAT_SIZE² Bytes, 0=ungesetzt 1-255=0.1-10m)
* 9 wie 8 + Gras-Textur-Map (SPLAT_SIZE² Bytes) + Slots (N×UTF)
*/
public final class MapIO {
@@ -50,7 +53,7 @@ public final class MapIO {
}
private static final int MAGIC = 0x424C4947; // "BLIG"
private static final int VERSION = 6;
private static final int VERSION = 9;
private MapIO() {}
@@ -104,6 +107,19 @@ public final class MapIO {
out.write(data.upperSplatB);
out.write(data.upperSplatA);
writeStrings(out, data.upperTextures);
// v7: gras-textur + standardhöhe
out.writeUTF(data.grassTexturePath != null ? data.grassTexturePath : "");
out.writeFloat(data.grassDefaultHeight);
// v8: gras-höhen-map
out.write(data.grassHeightMap);
// v9: gras-textur-slots + gras-textur-map
String[] slots = data.grassTextureSlots != null ? data.grassTextureSlots : new String[0];
// trim trailing empty entries
int slotEnd = slots.length;
while (slotEnd > 0 && (slots[slotEnd-1] == null || slots[slotEnd-1].isEmpty())) slotEnd--;
out.writeInt(slotEnd);
for (int i = 0; i < slotEnd; i++) out.writeUTF(slots[i] != null ? slots[i] : "");
out.write(data.grassTextureMap);
}
}
@@ -150,6 +166,19 @@ public final class MapIO {
// Ältere Maps: upperSplatR auf 255 initialisieren (Tex1 immer sichtbar)
java.util.Arrays.fill(data.upperSplatR, (byte) 255);
}
if (version >= 7) {
data.grassTexturePath = in.readUTF();
data.grassDefaultHeight = in.readFloat();
}
if (version >= 8) {
in.readFully(data.grassHeightMap);
}
if (version >= 9) {
int n = in.readInt();
data.grassTextureSlots = new String[n];
for (int i = 0; i < n; i++) data.grassTextureSlots[i] = in.readUTF();
in.readFully(data.grassTextureMap);
}
}
return data;
}

View File

@@ -11,5 +11,7 @@ public record PlacedModel(
/** Relativer Asset-Pfad zur exportierten j3o-Datei des Custom Meshes; "" wenn nicht verwendet. */
String meshFile,
/** AnimationLibrary-Clip-Key (z. B. "walk/Run"); "" = keine Animation. */
String animClip
String animClip,
boolean castShadow,
boolean receiveShadow
) {}

View File

@@ -8,10 +8,10 @@ import java.util.*;
* Liest und schreibt platzierte Modelle als tab-separierte Textdatei
* ({@code blight_objects.blo}) neben der Kartendatei.
*
* Spalten (seit v2):
* modelPath x y z rotY scale rotX rotZ solid texPath nmPath matPath meshFile
* Spalten (seit v3):
* modelPath x y z rotY scale rotX rotZ solid texPath nmPath matPath meshFile animClip castShadow receiveShadow
*
* Alte Dateien mit 6 Spalten (v1) werden gelesen; fehlende Felder erhalten Standardwerte.
* Alte Dateien mit 6 Spalten (v1/v2) werden gelesen; fehlende Felder erhalten Standardwerte.
*/
public final class PlacedModelIO {
@@ -25,18 +25,19 @@ public final class PlacedModelIO {
Path p = getPath();
Files.createDirectories(p.getParent());
try (BufferedWriter w = Files.newBufferedWriter(p)) {
w.write("# modelPath\tx\ty\tz\trotY\tscale\trotX\trotZ\tsolid\ttexPath\tnmPath\tmatPath\tmeshFile\tanimClip");
w.write("# modelPath\tx\ty\tz\trotY\tscale\trotX\trotZ\tsolid\ttexPath\tnmPath\tmatPath\tmeshFile\tanimClip\tcastShadow\treceiveShadow");
w.newLine();
for (PlacedModel m : models) {
w.write(String.format(Locale.ROOT,
"%s\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f\t%b\t%s\t%s\t%s\t%s\t%s%n",
"%s\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f\t%.5f\t%b\t%s\t%s\t%s\t%s\t%s\t%b\t%b%n",
m.modelPath(),
m.x(), m.y(), m.z(),
m.rotY(), m.scale(),
m.rotX(), m.rotZ(),
m.solid(),
nvl(m.texturePath()), nvl(m.normalMapPath()), nvl(m.materialPath()),
nvl(m.meshFile()), nvl(m.animClip())));
nvl(m.meshFile()), nvl(m.animClip()),
m.castShadow(), m.receiveShadow()));
}
}
}
@@ -63,11 +64,14 @@ public final class PlacedModelIO {
String texPath = f.length > 9 ? f[9] : "";
String nmPath = f.length > 10 ? f[10] : "";
String matPath = f.length > 11 ? f[11] : "";
String meshFile = f.length > 12 ? f[12] : "";
String animClip = f.length > 13 ? f[13] : "";
String meshFile = f.length > 12 ? f[12] : "";
String animClip = f.length > 13 ? f[13] : "";
boolean castShadow = f.length > 14 ? Boolean.parseBoolean(f[14]) : true;
boolean receiveShadow = f.length > 15 ? Boolean.parseBoolean(f[15]) : true;
list.add(new PlacedModel(modelPath, x, y, z,
rotY, rotX, rotZ, scale, solid,
texPath, nmPath, matPath, meshFile, animClip));
texPath, nmPath, matPath, meshFile, animClip,
castShadow, receiveShadow));
} catch (NumberFormatException ignored) {}
}
return list;

View File

@@ -1,7 +1,8 @@
package de.blight.common;
/** Unveränderliche Daten einer platzierten Wasseroberfläche (Teich, See). */
public record PlacedWater(
float x, float y, float z,
float width, float depth
) {}
/**
* Platzierte Wasserfläche.
* Die Form wird zur Laufzeit per Flood-Fill aus dem Geländenetz berechnet
* gespeichert werden nur Saatpunkt und Wasserstand.
*/
public record PlacedWater(float seedX, float seedZ, float waterHeight) {}

View File

@@ -0,0 +1,73 @@
package de.blight.common;
import java.io.*;
import java.nio.file.*;
import java.util.*;
/**
* Liest und schreibt platzierte Flüsse als Textdatei
* ({@code blight_rivers.blr}) neben der Kartendatei.
*
* Format: ein Fluss pro Zeile; Punkte getrennt durch {@code |};
* jeder Punkt: {@code x,y,z,width,uvSpeed} (5 Komma-getrennte Floats).
* Kommentarzeilen beginnen mit {@code #}.
*/
public final class RiverIO {
private RiverIO() {}
public static Path getPath() {
return MapIO.getMapPath().resolveSibling("blight_rivers.blr");
}
public static void save(List<List<RiverPoint>> rivers) throws IOException {
Path p = getPath();
Files.createDirectories(p.getParent());
try (BufferedWriter w = Files.newBufferedWriter(p)) {
w.write("# blight_rivers.blr Flussdaten");
w.newLine();
w.write("# Format: x,y,z,width,uvSpeed | x,y,z,width,uvSpeed | ...");
w.newLine();
for (List<RiverPoint> river : rivers) {
if (river == null || river.isEmpty()) continue;
StringBuilder sb = new StringBuilder();
for (int i = 0; i < river.size(); i++) {
if (i > 0) sb.append('|');
RiverPoint pt = river.get(i);
sb.append(String.format(Locale.ROOT, "%.5f,%.5f,%.5f,%.5f,%.5f",
pt.x(), pt.y(), pt.z(), pt.width(), pt.uvSpeed()));
}
w.write(sb.toString());
w.newLine();
}
}
}
public static List<List<RiverPoint>> load() throws IOException {
Path p = getPath();
if (!Files.exists(p)) return List.of();
List<List<RiverPoint>> result = new ArrayList<>();
for (String line : Files.readAllLines(p)) {
line = line.strip();
if (line.isEmpty() || line.startsWith("#")) continue;
String[] parts = line.split("\\|", -1);
List<RiverPoint> river = new ArrayList<>();
for (String part : parts) {
part = part.strip();
if (part.isEmpty()) continue;
String[] f = part.split(",", -1);
if (f.length < 5) continue;
try {
river.add(new RiverPoint(
Float.parseFloat(f[0]),
Float.parseFloat(f[1]),
Float.parseFloat(f[2]),
Float.parseFloat(f[3]),
Float.parseFloat(f[4])));
} catch (NumberFormatException ignored) {}
}
if (!river.isEmpty()) result.add(river);
}
return result;
}
}

View File

@@ -0,0 +1,9 @@
package de.blight.common;
/** Kontrollpunkt eines Flusses in Weltkoordinaten. */
public record RiverPoint(float x, float y, float z, float width, float uvSpeed) {
public static final float DEFAULT_WIDTH = 4.0f;
public static final float RIVER_SPEED = 0.4f;
public static final float WATERFALL_SPEED = 3.0f;
public boolean isWaterfall() { return uvSpeed >= 1.5f; }
}

View File

@@ -0,0 +1,71 @@
package de.blight.common;
import java.util.ArrayList;
import java.util.List;
/**
* Catmull-Rom-Spline-Glättung für Flusskontrollpunkte.
* Die Originalpunkte bleiben als Kontrollpunkte erhalten; subdivide()
* gibt eine dichtere Punktliste nur für die Visualisierung zurück.
*/
public final class RiverSpline {
/** Schritte pro Segment zwischen zwei Kontrollpunkten. */
private static final int STEPS = 8;
private RiverSpline() {}
/**
* Gibt eine Catmull-Rom-interpolierte Kopie der Punktliste zurück.
* Weniger als 2 Punkte werden unverändert zurückgegeben.
*/
public static List<RiverPoint> subdivide(List<RiverPoint> pts) {
int n = pts.size();
if (n < 2) return new ArrayList<>(pts);
List<RiverPoint> result = new ArrayList<>(n * STEPS);
for (int i = 0; i < n - 1; i++) {
// Ghost-Punkte an den Enden: Spiegelung des Nachbarn
RiverPoint p0 = (i > 0) ? pts.get(i - 1) : ghost(pts.get(i), pts.get(i + 1));
RiverPoint p1 = pts.get(i);
RiverPoint p2 = pts.get(i + 1);
RiverPoint p3 = (i < n - 2) ? pts.get(i + 2) : ghost(pts.get(i + 1), pts.get(i));
for (int s = 0; s < STEPS; s++) {
result.add(eval(p0, p1, p2, p3, (float) s / STEPS));
}
}
result.add(pts.get(n - 1)); // letzten Originalpunkt immer anhängen
return result;
}
// ── Catmull-Rom-Formel ────────────────────────────────────────────────────
private static RiverPoint eval(RiverPoint p0, RiverPoint p1,
RiverPoint p2, RiverPoint p3, float t) {
float t2 = t * t, t3 = t2 * t;
float b0 = -t3 + 2*t2 - t;
float b1 = 3*t3 - 5*t2 + 2;
float b2 = -3*t3 + 4*t2 + t;
float b3 = t3 - t2;
float x = 0.5f * (b0*p0.x() + b1*p1.x() + b2*p2.x() + b3*p3.x());
float y = 0.5f * (b0*p0.y() + b1*p1.y() + b2*p2.y() + b3*p3.y());
float z = 0.5f * (b0*p0.z() + b1*p1.z() + b2*p2.z() + b3*p3.z());
float w = p1.width() + t * (p2.width() - p1.width());
float s = p1.uvSpeed() + t * (p2.uvSpeed() - p1.uvSpeed());
return new RiverPoint(x, y, z, w, s);
}
/** Spiegelung von 'other' durch 'anchor' als Ghost-Kontrollpunkt. */
private static RiverPoint ghost(RiverPoint anchor, RiverPoint other) {
return new RiverPoint(
2*anchor.x() - other.x(),
2*anchor.y() - other.y(),
2*anchor.z() - other.z(),
anchor.width(),
anchor.uvSpeed()
);
}
}

View File

@@ -5,10 +5,11 @@ import java.nio.file.*;
import java.util.*;
/**
* Liest und schreibt platzierte Wasseroberflächen als tab-separierte Textdatei
* Liest und schreibt platzierte Wasserflächen als tab-separierte Textdatei
* ({@code blight_water.blw}) neben der Kartendatei.
*
* Spalten: x y z width depth
* Format: seedX seedZ waterHeight
* Die Form des Wasserkörpers wird per Flood-Fill zur Laufzeit rekonstruiert.
*/
public final class WaterBodyIO {
@@ -22,12 +23,11 @@ public final class WaterBodyIO {
Path p = getPath();
Files.createDirectories(p.getParent());
try (BufferedWriter w = Files.newBufferedWriter(p)) {
w.write("# x\ty\tz\twidth\tdepth");
w.write("# seedX\tseedZ\twaterHeight");
w.newLine();
for (PlacedWater b : bodies) {
w.write(String.format(Locale.ROOT,
"%.5f\t%.5f\t%.5f\t%.5f\t%.5f%n",
b.x(), b.y(), b.z(), b.width(), b.depth()));
w.write(String.format(Locale.ROOT, "%.5f\t%.5f\t%.5f%n",
b.seedX(), b.seedZ(), b.waterHeight()));
}
}
}
@@ -40,14 +40,12 @@ public final class WaterBodyIO {
line = line.strip();
if (line.isEmpty() || line.startsWith("#")) continue;
String[] f = line.split("\t", -1);
if (f.length < 5) continue;
if (f.length < 3) continue;
try {
list.add(new PlacedWater(
Float.parseFloat(f[0]),
Float.parseFloat(f[1]),
Float.parseFloat(f[2]),
Float.parseFloat(f[3]),
Float.parseFloat(f[4])));
Float.parseFloat(f[2])));
} catch (NumberFormatException ignored) {}
}
return list;

View File

@@ -21,11 +21,12 @@ import java.util.stream.Stream;
* "characterId": "hero",
* "name": "Der Held",
* "modelPath": "Models/hero.j3o",
* "animSetPath": "animations/sets/hero.j3o",
* "animSetPath": "human",
* ... (subclass fields via Gson)
* }
* Die Aktions-Zuweisung (IDLE → Clip-Name usw.) ist im AnimSet gespeichert
* (animSetPath.replaceAll(".j3o", "") + ".animset.json").
* animSetPath ist der Set-Name (ohne Pfad/Extension).
* Die Aktions-Zuweisung liegt in animations/sets/{animSetPath}.animset.json.
* Die Clip-Dateien liegen in animations/clips/{clipName}.j3o.
*/
public final class CharacterIO {