Tool und Oberflächen Konsolidierung angefangen
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -97,6 +97,13 @@ public class SharedInput {
|
||||
public record GrassVertexEdit(float screenX, float screenY, int action) {}
|
||||
public final ConcurrentLinkedQueue<GrassVertexEdit> grassVertexEditQueue = new ConcurrentLinkedQueue<>();
|
||||
|
||||
/** Globale Grasfarbe – frisches Gras (Spitze; Wurzel wird intern abgeleitet). Defaults: TIP_COLOR */
|
||||
public volatile float grassFreshR = 0.26f, grassFreshG = 0.72f, grassFreshB = 0.11f;
|
||||
/** Globale Grasfarbe – vertrocknet (Spitze). Defaults: DRY_TIP_COLOR */
|
||||
public volatile float grassDryR = 0.82f, grassDryG = 0.74f, grassDryB = 0.16f;
|
||||
/** true → GrassVertexState soll alle Chunks neu aufbauen */
|
||||
public volatile boolean grassColorsChanged = false;
|
||||
|
||||
// ── Farn-Generator ────────────────────────────────────────────────────────
|
||||
public record FernGenRequest(de.blight.editor.tree.FernOptions options, boolean exportAfter) {}
|
||||
public final ConcurrentLinkedQueue<FernGenRequest> fernGenQueue = new ConcurrentLinkedQueue<>();
|
||||
@@ -276,6 +283,8 @@ public class SharedInput {
|
||||
public volatile boolean objectSelectionChanged = false;
|
||||
/** Wird von JME3 gesetzt, wenn ein Objekt gerade neu platziert wurde (nicht nur selektiert). */
|
||||
public volatile boolean objectJustPlaced = false;
|
||||
/** Wird von JME3 gesetzt, wenn ein Objekt im Platzierungsmodus angeklickt wurde → JFX wechselt Panel zu Edit. */
|
||||
public volatile boolean objectClickedWhileInPlaceMode = false;
|
||||
|
||||
/** JavaFX → JME3: Modell-Pfad für nächste Platzierung (relativ zu blight-assets/src/main/resources/). */
|
||||
public volatile String pendingModelPath = null;
|
||||
@@ -1087,6 +1096,10 @@ public class SharedInput {
|
||||
/** 3D-Noise-Frequenz für die Kliff-Oberfläche (höher = feiner Detail). */
|
||||
public volatile float cliffRoughnessScale = 0.12f;
|
||||
|
||||
// ── Universelles Auswahl-Werkzeug ─────────────────────────────────────────
|
||||
/** activeLayer==33 → Auswahl-Modus: kein Terrain-Editing, Klick selektiert Objekte */
|
||||
public static final int LAYER_AUSWAHL = 33;
|
||||
|
||||
// ── Voxel-Textur-Malen ────────────────────────────────────────────────────
|
||||
/** Parallel-Queue zu textureEditQueue – wird von submitEdit(layer=4) mitbefüllt,
|
||||
* damit VoxelEditorState unabhängig vom Terrain seine Splatmap beschreiben kann. */
|
||||
|
||||
@@ -91,6 +91,10 @@ public class GrassVertexState extends BaseAppState {
|
||||
private final Node[] chunkNodes = new Node[CHUNK_COUNT];
|
||||
private final boolean[] dirtyChunks = new boolean[CHUNK_COUNT];
|
||||
|
||||
// Effektive Grasfarben – werden aus SharedInput übernommen, Wurzel wird abgeleitet
|
||||
private float freshTipR = TIP_COLOR.r, freshTipG = TIP_COLOR.g, freshTipB = TIP_COLOR.b;
|
||||
private float dryTipR = DRY_TIP_COLOR.r, dryTipG = DRY_TIP_COLOR.g, dryTipB = DRY_TIP_COLOR.b;
|
||||
|
||||
public GrassVertexState(SharedInput input) {
|
||||
this.input = input;
|
||||
for (int i = 0; i < CHUNK_COUNT; i++) chunkBlades[i] = new ArrayList<>();
|
||||
@@ -168,6 +172,12 @@ public class GrassVertexState extends BaseAppState {
|
||||
|
||||
@Override
|
||||
public void update(float tpf) {
|
||||
if (input.grassColorsChanged) {
|
||||
input.grassColorsChanged = false;
|
||||
freshTipR = input.grassFreshR; freshTipG = input.grassFreshG; freshTipB = input.grassFreshB;
|
||||
dryTipR = input.grassDryR; dryTipG = input.grassDryG; dryTipB = input.grassDryB;
|
||||
for (int ci = 0; ci < CHUNK_COUNT; ci++) dirtyChunks[ci] = !chunkBlades[ci].isEmpty();
|
||||
}
|
||||
processBrushEdits();
|
||||
rebuildDirtyChunks();
|
||||
updateChunkVisibility();
|
||||
@@ -399,9 +409,15 @@ public class GrassVertexState extends BaseAppState {
|
||||
float[] texCoords = new float[vertCount * 2];
|
||||
int[] indices = new int[indexCount];
|
||||
|
||||
// Wurzelfarbe = Spitze * fester Faktor (entspricht den ursprünglichen statischen Verhältnissen)
|
||||
float frR = freshTipR * 0.308f, frG = freshTipG * 0.472f, frB = freshTipB * 0.364f;
|
||||
float drR = dryTipR * 0.549f, drG = dryTipG * 0.473f, drB = dryTipB * 0.500f;
|
||||
|
||||
int vi = 0, ii = 0;
|
||||
for (GrassVertexBlade blade : blades) {
|
||||
buildTuft(positions, normals, colors, texCoords, indices, vi, ii, blade);
|
||||
buildTuft(positions, normals, colors, texCoords, indices, vi, ii, blade,
|
||||
frR, frG, frB, freshTipR, freshTipG, freshTipB,
|
||||
drR, drG, drB, dryTipR, dryTipG, dryTipB);
|
||||
vi += BLADES_PER_TUFT * (SEGMENTS + 1) * 2;
|
||||
ii += BLADES_PER_TUFT * SEGMENTS * 6;
|
||||
}
|
||||
@@ -560,7 +576,11 @@ public class GrassVertexState extends BaseAppState {
|
||||
// ── Mesh-Generierung (gemeinsame Logik, auch von GrassVertexRenderState genutzt) ──
|
||||
|
||||
static void buildTuft(float[] pos, float[] nrm, float[] col, float[] tex, int[] idx,
|
||||
int vi, int ii, GrassVertexBlade blade) {
|
||||
int vi, int ii, GrassVertexBlade blade,
|
||||
float frshRootR, float frshRootG, float frshRootB,
|
||||
float frshTipR, float frshTipG, float frshTipB,
|
||||
float dryRootR, float dryRootG, float dryRootB,
|
||||
float dryTipR, float dryTipG, float dryTipB) {
|
||||
float x = blade.x(), y = blade.y(), z = blade.z(), h = blade.height();
|
||||
float baseHW = h * WIDTH_FACTOR * 0.5f;
|
||||
float tAngle = (float) (((x * 127.1f + z * 311.7f) % (Math.PI * 2) + Math.PI * 2) % (Math.PI * 2));
|
||||
@@ -626,8 +646,12 @@ public class GrassVertexState extends BaseAppState {
|
||||
|
||||
int svi = bladeVi + s * 2;
|
||||
float dry = blade.dryness();
|
||||
setV(pos, nrm, col, tex, svi, spX - cosA * hw, spY, spZ - sinA * hw, nx, ny, nz, t, dry);
|
||||
setV(pos, nrm, col, tex, svi+1, spX + cosA * hw, spY, spZ + sinA * hw, nx, ny, nz, t, dry);
|
||||
setV(pos, nrm, col, tex, svi, spX - cosA * hw, spY, spZ - sinA * hw, nx, ny, nz, t, dry,
|
||||
frshRootR, frshRootG, frshRootB, frshTipR, frshTipG, frshTipB,
|
||||
dryRootR, dryRootG, dryRootB, dryTipR, dryTipG, dryTipB);
|
||||
setV(pos, nrm, col, tex, svi+1, spX + cosA * hw, spY, spZ + sinA * hw, nx, ny, nz, t, dry,
|
||||
frshRootR, frshRootG, frshRootB, frshTipR, frshTipG, frshTipB,
|
||||
dryRootR, dryRootG, dryRootB, dryTipR, dryTipG, dryTipB);
|
||||
|
||||
if (s < SEGMENTS) {
|
||||
int sii = bladeIi + s * 6;
|
||||
@@ -651,7 +675,11 @@ public class GrassVertexState extends BaseAppState {
|
||||
|
||||
private static void setV(float[] pos, float[] nrm, float[] col, float[] tex, int vi,
|
||||
float x, float y, float z, float nx, float ny, float nz,
|
||||
float wf, float dryness) {
|
||||
float wf, float dryness,
|
||||
float frshRootR, float frshRootG, float frshRootB,
|
||||
float frshTipR, float frshTipG, float frshTipB,
|
||||
float dryRootR, float dryRootG, float dryRootB,
|
||||
float dryTipR, float dryTipG, float dryTipB) {
|
||||
int pi = vi * 3;
|
||||
pos[pi] = x; pos[pi+1] = y; pos[pi+2] = z;
|
||||
|
||||
@@ -659,16 +687,15 @@ public class GrassVertexState extends BaseAppState {
|
||||
nrm[ni] = nx; nrm[ni+1] = ny; nrm[ni+2] = nz;
|
||||
|
||||
int ci = vi * 4;
|
||||
float gr = ROOT_COLOR.r + (TIP_COLOR.r - ROOT_COLOR.r) * wf;
|
||||
float gg = ROOT_COLOR.g + (TIP_COLOR.g - ROOT_COLOR.g) * wf;
|
||||
float gb = ROOT_COLOR.b + (TIP_COLOR.b - ROOT_COLOR.b) * wf;
|
||||
float dr = DRY_ROOT_COLOR.r + (DRY_TIP_COLOR.r - DRY_ROOT_COLOR.r) * wf;
|
||||
float dg = DRY_ROOT_COLOR.g + (DRY_TIP_COLOR.g - DRY_ROOT_COLOR.g) * wf;
|
||||
float db = DRY_ROOT_COLOR.b + (DRY_TIP_COLOR.b - DRY_ROOT_COLOR.b) * wf;
|
||||
float gr = frshRootR + (frshTipR - frshRootR) * wf;
|
||||
float gg = frshRootG + (frshTipG - frshRootG) * wf;
|
||||
float gb = frshRootB + (frshTipB - frshRootB) * wf;
|
||||
float dr = dryRootR + (dryTipR - dryRootR) * wf;
|
||||
float dg = dryRootG + (dryTipG - dryRootG) * wf;
|
||||
float db = dryRootB + (dryTipB - dryRootB) * wf;
|
||||
float vr = VERY_DRY_ROOT_COLOR.r + (VERY_DRY_TIP_COLOR.r - VERY_DRY_ROOT_COLOR.r) * wf;
|
||||
float vg = VERY_DRY_ROOT_COLOR.g + (VERY_DRY_TIP_COLOR.g - VERY_DRY_ROOT_COLOR.g) * wf;
|
||||
float vb = VERY_DRY_ROOT_COLOR.b + (VERY_DRY_TIP_COLOR.b - VERY_DRY_ROOT_COLOR.b) * wf;
|
||||
// Zwei-Segment-Gradient: 0→0.5 = grün→goldgelb, 0.5→1.0 = goldgelb→dunkelbraun
|
||||
float fr, fg, fb;
|
||||
if (dryness <= 0.5f) {
|
||||
float t = dryness * 2f;
|
||||
|
||||
@@ -381,10 +381,13 @@ public class PlayToolState extends BaseAppState {
|
||||
VoxelEditorState ves = getStateManager().getState(VoxelEditorState.class);
|
||||
if (ves != null) {
|
||||
Vector3f pt = ves.clickAt(ray);
|
||||
if (pt != null && ves.terrainTypeAt(pt.x, pt.z) == VoxelEditorState.TerrainType.VOXEL_UNBAKED) {
|
||||
return null;
|
||||
if (pt != null) {
|
||||
if (ves.terrainTypeAt(pt.x, pt.z) == VoxelEditorState.TerrainType.VOXEL_UNBAKED) {
|
||||
return null;
|
||||
}
|
||||
return pt;
|
||||
}
|
||||
return pt;
|
||||
// ves hat kein Terrain-Geometry — Fallback auf Base-Terrain
|
||||
}
|
||||
if (terrain == null) return null;
|
||||
CollisionResults hits = new CollisionResults();
|
||||
|
||||
@@ -395,7 +395,8 @@ public class SceneObjectState extends BaseAppState {
|
||||
handleBenchSitzLayer();
|
||||
|
||||
boolean isObjectLayer = input.activeLayer == SharedInput.LAYER_OBJECTS
|
||||
|| input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT;
|
||||
|| input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT
|
||||
|| input.activeLayer == SharedInput.LAYER_AUSWAHL;
|
||||
if (!isObjectLayer) return;
|
||||
|
||||
// Animation-Clip-Zuweisung von JavaFX
|
||||
@@ -680,11 +681,23 @@ public class SceneObjectState extends BaseAppState {
|
||||
input.objectSelectionMode = SharedInput.SEL_MODE_OBJECT;
|
||||
if (input.objectSelectionMode != SharedInput.SEL_MODE_OBJECT)
|
||||
applySubSelection(closest);
|
||||
if (input.activeLayer == SharedInput.LAYER_OBJECTS
|
||||
|| input.activeLayer == SharedInput.LAYER_AUSWAHL) {
|
||||
input.activeLayer = SharedInput.LAYER_OBJECTS_EDIT;
|
||||
input.objectClickedWhileInPlaceMode = true;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Terrain-Treffer – Platzieren nur im Platzieren-Modus
|
||||
// 3. Terrain-Treffer – im Edit-Modus: zurück zu Place-Modus, deselektieren
|
||||
if (input.activeLayer == SharedInput.LAYER_OBJECTS_EDIT) {
|
||||
input.activeLayer = SharedInput.LAYER_OBJECTS;
|
||||
deselectAll();
|
||||
return;
|
||||
}
|
||||
// Auswahl-Modus: kein Objekt angeklickt → nur deselektieren, Layer bleibt
|
||||
if (input.activeLayer == SharedInput.LAYER_AUSWAHL) { deselectAll(); return; }
|
||||
if (input.activeLayer != SharedInput.LAYER_OBJECTS) { deselectAll(); return; }
|
||||
|
||||
String modelPath = input.pendingModelPath;
|
||||
|
||||
@@ -378,6 +378,9 @@ public class TerrainEditorState extends BaseAppState {
|
||||
}
|
||||
|
||||
input.loadingStatus = "Baue Szene...";
|
||||
VoxelEditorState ves = app.getStateManager().getState(VoxelEditorState.class);
|
||||
if (ves != null) ves.setTerrainNode(terrain);
|
||||
|
||||
PlayToolState playToolState = app.getStateManager().getState(PlayToolState.class);
|
||||
if (playToolState != null) playToolState.setTerrain(terrain);
|
||||
|
||||
@@ -1394,6 +1397,7 @@ public class TerrainEditorState extends BaseAppState {
|
||||
|| layer == SharedInput.LAYER_VOXEL || layer == SharedInput.LAYER_STONE
|
||||
|| layer == SharedInput.LAYER_BED_LIEGE
|
||||
|| layer == SharedInput.LAYER_BENCH_SITZ
|
||||
|| layer == SharedInput.LAYER_AUSWAHL
|
||||
|| mx < 0) {
|
||||
brushIndicator.setCullHint(Spatial.CullHint.Always);
|
||||
return;
|
||||
|
||||
@@ -6,7 +6,7 @@ public class GrassVertexTool extends EditorTool {
|
||||
|
||||
public final ToolParameter brushRadius = new ToolParameter("Pinselradius", 5.0, 1.0, 50.0);
|
||||
public final ToolParameter bladeHeight = new ToolParameter("Halmhöhe", 0.6, 0.1, 2.0);
|
||||
public final ToolParameter density = new ToolParameter("Dichte", 5.0, 1.0, 100.0);
|
||||
public final ToolParameter density = new ToolParameter("Dichte", 50.0, 1.0, 100.0);
|
||||
public final ToolParameter dryness = new ToolParameter("Vertrocknet %", 0.0, 0.0, 100.0);
|
||||
/** 1.0 = exakt gleiche Höhe, 0.0 = ±25 % Zufallsvariation */
|
||||
public final ToolParameter uniformity = new ToolParameter("Gleichmäßigkeit", 1.0, 0.0, 1.0);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package de.blight.editor.ui;
|
||||
|
||||
import de.blight.common.MonologueIO;
|
||||
import de.blight.common.model.*;
|
||||
import de.blight.common.model.quests.Quest;
|
||||
import de.blight.common.model.quests.QuestIO;
|
||||
@@ -21,7 +22,10 @@ import javafx.scene.shape.Rectangle;
|
||||
import javafx.scene.text.Text;
|
||||
import javafx.stage.Modality;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.time.LocalDate;
|
||||
import java.util.*;
|
||||
|
||||
/**
|
||||
@@ -41,6 +45,8 @@ public class DialogEditorView extends BorderPane {
|
||||
|
||||
private ToggleButton listBtn;
|
||||
private ToggleButton graphBtn;
|
||||
private Button skriptBtn;
|
||||
private Button ttsBtn;
|
||||
|
||||
// ── List-mode ─────────────────────────────────────────────────────────────
|
||||
|
||||
@@ -105,6 +111,7 @@ public class DialogEditorView extends BorderPane {
|
||||
normalizeReferences();
|
||||
refreshOptionList();
|
||||
clearDetailForm();
|
||||
setCharacterLoaded(true);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
@@ -112,8 +119,15 @@ public class DialogEditorView extends BorderPane {
|
||||
allOptions.clear();
|
||||
rootIds.clear();
|
||||
selectedId = null;
|
||||
currentNpcId = "";
|
||||
refreshOptionList();
|
||||
clearDetailForm();
|
||||
setCharacterLoaded(false);
|
||||
}
|
||||
|
||||
public void setCharacterLoaded(boolean loaded) {
|
||||
if (skriptBtn != null) skriptBtn.setDisable(!loaded);
|
||||
if (ttsBtn != null) ttsBtn.setDisable(!loaded);
|
||||
}
|
||||
|
||||
public void exportToNpc(NPC npc) {
|
||||
@@ -162,8 +176,21 @@ public class DialogEditorView extends BorderPane {
|
||||
Button newBtn = new Button("+ Option");
|
||||
newBtn.setOnAction(e -> createOption(false));
|
||||
|
||||
skriptBtn = new Button("Skript");
|
||||
skriptBtn.setStyle("-fx-background-color: #3a4a6a; -fx-text-fill: white;");
|
||||
skriptBtn.setDisable(true);
|
||||
skriptBtn.setOnAction(e -> { exportNpcScript(); exportHeroScript(); });
|
||||
|
||||
ttsBtn = new Button("TTS");
|
||||
ttsBtn.setStyle("-fx-background-color: #5a3a2a; -fx-text-fill: white;");
|
||||
ttsBtn.setDisable(true);
|
||||
ttsBtn.setOnAction(e -> new TtsGeneratorDialog(currentNpcId,
|
||||
new LinkedHashMap<>(allOptions)).showAndWait());
|
||||
|
||||
HBox bar = new HBox(8, listBtn, graphBtn,
|
||||
new Separator(Orientation.VERTICAL), newRootBtn, newBtn);
|
||||
new Separator(Orientation.VERTICAL), newRootBtn, newBtn,
|
||||
new Separator(Orientation.VERTICAL), skriptBtn,
|
||||
new Separator(Orientation.VERTICAL), ttsBtn);
|
||||
bar.setPadding(new Insets(8));
|
||||
bar.setAlignment(Pos.CENTER_LEFT);
|
||||
bar.setStyle("-fx-background-color: #1a1a2a; -fx-border-color: #555;"
|
||||
@@ -1249,6 +1276,149 @@ public class DialogEditorView extends BorderPane {
|
||||
return idx == 0 ? base + suffix : base + suffix + "." + idx;
|
||||
}
|
||||
|
||||
// ── Skript-Export ──────────────────────────────────────────────────────────
|
||||
|
||||
private void exportNpcScript() {
|
||||
saveCurrentForm();
|
||||
if (currentNpcId.isBlank() || allOptions.isEmpty()) {
|
||||
Dialogs.alert(Alert.AlertType.WARNING,
|
||||
"Kein NPC geladen oder keine Dialog-Optionen vorhanden.", ButtonType.OK).showAndWait();
|
||||
return;
|
||||
}
|
||||
Map<String, String> texts = loadTexts();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("=== NPC-Skript: ").append(currentNpcId).append(" ===\n");
|
||||
sb.append("Exportiert: ").append(LocalDate.now()).append("\n\n");
|
||||
|
||||
for (Map.Entry<String, DialogOption> entry : allOptions.entrySet()) {
|
||||
DialogOption opt = entry.getValue();
|
||||
String optId = entry.getKey();
|
||||
String base = "dialog." + currentNpcId + "." + optId;
|
||||
List<String> npcKeys = resolveStepKeys(opt.getNpcSteps(), opt.getTextNpc(), base, ".textnpc");
|
||||
if (npcKeys.isEmpty()) continue;
|
||||
|
||||
sb.append("--- ").append(optId);
|
||||
if (rootIds.contains(optId)) sb.append(" [ROOT]");
|
||||
sb.append(" ---\n");
|
||||
appendStepLines(sb, npcKeys, "NPC", texts);
|
||||
sb.append("\n");
|
||||
}
|
||||
|
||||
writeScript(ProjectRoot.resolve("dialog_script_npc_" + currentNpcId + ".txt"), sb.toString());
|
||||
}
|
||||
|
||||
private void exportHeroScript() {
|
||||
Map<String, String> texts = loadTexts();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.append("=== Held-Skript ===\n");
|
||||
sb.append("Exportiert: ").append(LocalDate.now()).append("\n\n");
|
||||
|
||||
Path charDir = ProjectRoot.resolve("blight-assets", "src", "main", "resources", "character");
|
||||
for (GameCharacter gc : CharacterIO.loadAll(charDir)) {
|
||||
if (!(gc instanceof NPC npc)) continue;
|
||||
if (npc.getDialogOptions() == null || npc.getDialogOptions().isEmpty()) continue;
|
||||
|
||||
boolean npcHeaderWritten = false;
|
||||
for (Map.Entry<String, DialogOption> entry : npc.getDialogOptions().entrySet()) {
|
||||
String optId = entry.getKey();
|
||||
DialogOption opt = entry.getValue();
|
||||
String base = "dialog." + npc.getCharacterId() + "." + optId;
|
||||
List<String> heroKeys = resolveStepKeys(opt.getHeroSteps(), opt.getTextHero(), base, ".textmainchar");
|
||||
if (heroKeys.isEmpty()) continue;
|
||||
|
||||
if (!npcHeaderWritten) {
|
||||
sb.append("=== NPC: ").append(npc.getCharacterId()).append(" ===\n\n");
|
||||
npcHeaderWritten = true;
|
||||
}
|
||||
sb.append("--- ").append(optId).append(" ---\n");
|
||||
appendStepLines(sb, heroKeys, "HELD", texts);
|
||||
sb.append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
List<Monologue> monologues = MonologueIO.load();
|
||||
if (!monologues.isEmpty()) {
|
||||
sb.append("=== Monologe ===\n\n");
|
||||
for (Monologue m : monologues) {
|
||||
if (m.getHeroSteps() == null || m.getHeroSteps().isEmpty()) continue;
|
||||
sb.append("--- ").append(m.getId()).append(" ---\n");
|
||||
List<String> keys = m.getHeroSteps().stream()
|
||||
.filter(s -> s.getText() != null && !s.getText().id().isBlank())
|
||||
.map(s -> s.getText().id())
|
||||
.toList();
|
||||
appendStepLines(sb, keys, "HELD", texts);
|
||||
sb.append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
writeScript(ProjectRoot.resolve("dialog_script_held.txt"), sb.toString());
|
||||
}
|
||||
|
||||
private Map<String, String> loadTexts() {
|
||||
try {
|
||||
Path msgFile = ProjectRoot.resolve(
|
||||
"blight-lang", "src", "main", "resources", "lang", "messages_de.properties");
|
||||
if (Files.exists(msgFile)) return new LinkedHashMap<>(TextBundleIO.load(msgFile).getEntries());
|
||||
} catch (Exception ignored) {}
|
||||
return new LinkedHashMap<>();
|
||||
}
|
||||
|
||||
private static void appendStepLines(StringBuilder sb, List<String> keys,
|
||||
String label, Map<String, String> texts) {
|
||||
for (int i = 0; i < keys.size(); i++) {
|
||||
String key = keys.get(i);
|
||||
sb.append(keys.size() > 1 ? " " + label + " " + (i + 1) + ":\n" : " " + label + ":\n");
|
||||
sb.append(" Key: ").append(key).append("\n");
|
||||
sb.append(" Text: \"").append(texts.getOrDefault(key, "(nicht übersetzt)")).append("\"\n");
|
||||
sb.append(" Audio: ").append(audioPathForKey(key)).append("\n");
|
||||
}
|
||||
}
|
||||
|
||||
private void writeScript(Path out, String content) {
|
||||
try {
|
||||
Files.writeString(out, content);
|
||||
Dialogs.alert(Alert.AlertType.INFORMATION,
|
||||
"Skript exportiert nach:\n" + out, ButtonType.OK).showAndWait();
|
||||
} catch (IOException ex) {
|
||||
Dialogs.alert(Alert.AlertType.ERROR,
|
||||
"Fehler beim Schreiben: " + ex.getMessage(), ButtonType.OK).showAndWait();
|
||||
}
|
||||
}
|
||||
|
||||
/** Liefert die Step-Keys einer Option: erst aus heroSteps/npcSteps, dann textHero/textNpc, dann Ableitungsfall. */
|
||||
static List<String> resolveStepKeys(List<DialogStep> steps, TextReference legacy,
|
||||
String base, String suffix) {
|
||||
if (steps != null && !steps.isEmpty()) {
|
||||
List<String> keys = new ArrayList<>();
|
||||
for (DialogStep s : steps) {
|
||||
if (s.getText() != null && !s.getText().id().isBlank()) keys.add(s.getText().id());
|
||||
}
|
||||
if (!keys.isEmpty()) return keys;
|
||||
}
|
||||
if (legacy != null && !legacy.id().isBlank()) return List.of(legacy.id());
|
||||
return List.of(base + suffix);
|
||||
}
|
||||
|
||||
private static String audioPathForKey(String key) {
|
||||
if (key == null) return "—";
|
||||
if (key.startsWith("dialog.")) {
|
||||
String without = key.substring("dialog.".length());
|
||||
int dot = without.indexOf('.');
|
||||
if (dot < 0) return "—";
|
||||
return "audio/dialog/" + without.substring(0, dot) + "/"
|
||||
+ without.substring(dot + 1).replace('.', '_') + ".ogg";
|
||||
}
|
||||
if (key.startsWith("monologue.")) {
|
||||
String without = key.substring("monologue.".length());
|
||||
int dot = without.indexOf('.');
|
||||
if (dot < 0) return "audio/monologue/" + without + ".ogg";
|
||||
return "audio/monologue/" + without.substring(0, dot) + "_"
|
||||
+ without.substring(dot + 1).replace('.', '_') + ".ogg";
|
||||
}
|
||||
return "—";
|
||||
}
|
||||
|
||||
|
||||
private void refreshStepKeys(String base) {
|
||||
if (heroStepsView == null || npcStepsView == null) {
|
||||
return;
|
||||
|
||||
@@ -6,6 +6,7 @@ import de.blight.common.model.TextBundle;
|
||||
import de.blight.common.model.TextBundleIO;
|
||||
import de.blight.common.model.TextKeyStore;
|
||||
import de.blight.common.model.TextRegistry;
|
||||
import de.blight.editor.ProjectRoot;
|
||||
import javafx.animation.PauseTransition;
|
||||
import javafx.beans.property.SimpleStringProperty;
|
||||
import javafx.collections.FXCollections;
|
||||
@@ -139,7 +140,11 @@ public class LocalizationEditorView extends BorderPane {
|
||||
Button sortBtn = new Button("A->Z");
|
||||
sortBtn.setOnAction(e -> audioData.sort(Comparator.comparing(r -> r[0])));
|
||||
|
||||
HBox subBar = new HBox(6, addKeyBtn, delKeyBtn, sortBtn);
|
||||
Button fillDialogBtn = new Button("Dialog-Schluessel befuellen");
|
||||
fillDialogBtn.setStyle("-fx-background-color: #3a4a6a; -fx-text-fill: white;");
|
||||
fillDialogBtn.setOnAction(e -> autoFillDialogKeys());
|
||||
|
||||
HBox subBar = new HBox(6, addKeyBtn, delKeyBtn, sortBtn, fillDialogBtn);
|
||||
subBar.setPadding(new Insets(6, 12, 6, 12));
|
||||
subBar.setStyle("-fx-background-color: #1a1a2a; -fx-border-color: #444; -fx-border-width: 0 0 1 0;");
|
||||
|
||||
@@ -177,9 +182,20 @@ public class LocalizationEditorView extends BorderPane {
|
||||
new FileChooser.ExtensionFilter("Audio", "*.ogg", "*.wav", "*.mp3"),
|
||||
new FileChooser.ExtensionFilter("Alle Dateien", "*.*")
|
||||
);
|
||||
Path audioRoot = ProjectRoot.resolve(
|
||||
"blight-assets", "src", "main", "resources");
|
||||
File initialDir = audioRoot.resolve("audio").resolve("dialog").toFile();
|
||||
if (initialDir.isDirectory()) fc.setInitialDirectory(initialDir);
|
||||
File file = fc.showOpenDialog(getScene().getWindow());
|
||||
if (file != null) {
|
||||
audioData.get(idx)[1] = file.getAbsolutePath();
|
||||
String path;
|
||||
try {
|
||||
path = audioRoot.relativize(file.toPath())
|
||||
.toString().replace('\\', '/');
|
||||
} catch (IllegalArgumentException e) {
|
||||
path = file.getAbsolutePath();
|
||||
}
|
||||
audioData.get(idx)[1] = path;
|
||||
audioTable.refresh();
|
||||
scheduleSave();
|
||||
}
|
||||
@@ -349,6 +365,38 @@ public class LocalizationEditorView extends BorderPane {
|
||||
if (sel != null) { audioData.remove(sel); scheduleSave(); }
|
||||
}
|
||||
|
||||
private void autoFillDialogKeys() {
|
||||
Set<String> existing = new HashSet<>();
|
||||
for (String[] row : audioData) existing.add(row[0]);
|
||||
|
||||
List<String> added = new ArrayList<>();
|
||||
for (String key : TextKeyStore.getKeys()) {
|
||||
if (!key.startsWith("dialog.")) continue;
|
||||
if (existing.contains(key)) continue;
|
||||
audioData.add(new String[]{key, keyToAudioPath(key)});
|
||||
existing.add(key);
|
||||
added.add(key);
|
||||
}
|
||||
if (!added.isEmpty()) {
|
||||
audioData.sort(Comparator.comparing(r -> r[0]));
|
||||
scheduleSave();
|
||||
}
|
||||
Dialogs.alert(Alert.AlertType.INFORMATION,
|
||||
added.isEmpty() ? "Alle Dialog-Schluessel bereits vorhanden."
|
||||
: added.size() + " Schluessel hinzugefuegt.",
|
||||
ButtonType.OK).showAndWait();
|
||||
}
|
||||
|
||||
private static String keyToAudioPath(String key) {
|
||||
if (key == null || !key.startsWith("dialog.")) return "";
|
||||
String without = key.substring("dialog.".length());
|
||||
int dot = without.indexOf('.');
|
||||
if (dot < 0) return "";
|
||||
String npcId = without.substring(0, dot);
|
||||
String rest = without.substring(dot + 1).replace('.', '_');
|
||||
return "audio/dialog/" + npcId + "/" + rest + ".ogg";
|
||||
}
|
||||
|
||||
// Auto-save
|
||||
|
||||
private void scheduleSave() {
|
||||
|
||||
@@ -0,0 +1,424 @@
|
||||
package de.blight.editor.ui;
|
||||
|
||||
import de.blight.common.BlightHome;
|
||||
import de.blight.common.MonologueIO;
|
||||
import de.blight.common.model.*;
|
||||
import de.blight.editor.ProjectRoot;
|
||||
import javafx.application.Platform;
|
||||
import javafx.concurrent.Task;
|
||||
import javafx.geometry.Insets;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.layout.*;
|
||||
import javafx.stage.DirectoryChooser;
|
||||
import javafx.stage.Modality;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.*;
|
||||
import java.util.*;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
/**
|
||||
* Generiert TTS-Platzhalter-Audio via Piper für alle Dialog- oder Monolog-Zeilen.
|
||||
*
|
||||
* <ul>
|
||||
* <li>NPC-Modus: NPC-Zeilen des aktuell geladenen NPCs</li>
|
||||
* <li>Held-Modus: alle Held-Zeilen aus allen NPCs + Monologe</li>
|
||||
* </ul>
|
||||
*
|
||||
* Ausgabe: OGG (wenn ffmpeg verfügbar) oder WAV als Fallback.
|
||||
* Pfade werden automatisch in {@code audio_de.properties} eingetragen.
|
||||
*/
|
||||
public class TtsGeneratorDialog extends Dialog<Void> {
|
||||
|
||||
private static final Path PREFS_FILE = BlightHome.resolve("config", "editor.prefs");
|
||||
|
||||
private final String npcId;
|
||||
private final Map<String, DialogOption> npcOptions;
|
||||
|
||||
private TextField piperField;
|
||||
private TextField modelsDirField;
|
||||
private ComboBox<String> modelCombo; // zeigt Dateinamen, Wert = voller Pfad
|
||||
private CheckBox overwriteCheck;
|
||||
private TextArea logArea;
|
||||
private Button generateBtn;
|
||||
private ProgressBar progressBar;
|
||||
|
||||
public TtsGeneratorDialog(String npcId, Map<String, DialogOption> npcOptions) {
|
||||
this.npcId = npcId != null ? npcId : "";
|
||||
this.npcOptions = npcOptions != null ? new LinkedHashMap<>(npcOptions) : Map.of();
|
||||
|
||||
setTitle("TTS-Platzhalter generieren (Piper)");
|
||||
initModality(Modality.APPLICATION_MODAL);
|
||||
initOwner(Dialogs.primaryWindow());
|
||||
setResizable(true);
|
||||
|
||||
buildUI();
|
||||
loadPrefs();
|
||||
|
||||
getDialogPane().getButtonTypes().add(ButtonType.CLOSE);
|
||||
setResultConverter(bt -> null);
|
||||
}
|
||||
|
||||
// ── UI ────────────────────────────────────────────────────────────────────
|
||||
|
||||
private void buildUI() {
|
||||
// Piper binary
|
||||
piperField = new TextField();
|
||||
piperField.setPromptText("/pfad/zur/piper-binary oder piper");
|
||||
|
||||
// Models directory + rescan
|
||||
modelsDirField = new TextField();
|
||||
modelsDirField.setPromptText("/pfad/zu/models/");
|
||||
Button dirBrowseBtn = new Button("...");
|
||||
dirBrowseBtn.setOnAction(e -> browseModelsDir());
|
||||
Button rescanBtn = new Button("⟳");
|
||||
rescanBtn.setTooltip(new Tooltip("Modelle neu einlesen"));
|
||||
rescanBtn.setOnAction(e -> refreshModels());
|
||||
HBox dirRow = new HBox(4, modelsDirField, dirBrowseBtn, rescanBtn);
|
||||
HBox.setHgrow(modelsDirField, Priority.ALWAYS);
|
||||
|
||||
// Model chooser
|
||||
modelCombo = new ComboBox<>();
|
||||
modelCombo.setMaxWidth(Double.MAX_VALUE);
|
||||
modelCombo.setPromptText("Modell wählen...");
|
||||
|
||||
overwriteCheck = new CheckBox("Vorhandene Dateien überschreiben");
|
||||
|
||||
progressBar = new ProgressBar(0);
|
||||
progressBar.setMaxWidth(Double.MAX_VALUE);
|
||||
|
||||
logArea = new TextArea();
|
||||
logArea.setEditable(false);
|
||||
logArea.setPrefHeight(200);
|
||||
logArea.setStyle("-fx-control-inner-background: #1a1a2a; -fx-text-fill: #ccc;"
|
||||
+ " -fx-font-family: monospace; -fx-font-size: 11;");
|
||||
|
||||
generateBtn = new Button("Generieren");
|
||||
generateBtn.setStyle("-fx-background-color: #3a7a3a; -fx-text-fill: white;");
|
||||
generateBtn.setMaxWidth(Double.MAX_VALUE);
|
||||
generateBtn.setOnAction(e -> startGeneration());
|
||||
|
||||
VBox content = new VBox(8);
|
||||
content.setPadding(new Insets(16));
|
||||
content.setPrefWidth(580);
|
||||
content.getChildren().addAll(
|
||||
row("Piper-Binary:", piperField),
|
||||
row("Modell-Verzeichnis:", dirRow),
|
||||
row("Stimme:", modelCombo),
|
||||
new Separator(),
|
||||
overwriteCheck,
|
||||
new Separator(),
|
||||
generateBtn,
|
||||
progressBar,
|
||||
logArea
|
||||
);
|
||||
getDialogPane().setContent(content);
|
||||
|
||||
piperField.focusedProperty().addListener((obs, o, n) -> { if (!n) savePrefs(); });
|
||||
modelsDirField.focusedProperty().addListener((obs, o, n) -> { if (!n) { refreshModels(); savePrefs(); } });
|
||||
}
|
||||
|
||||
private void browseModelsDir() {
|
||||
DirectoryChooser dc = new DirectoryChooser();
|
||||
dc.setTitle("Piper-Modell-Verzeichnis wählen");
|
||||
String cur = modelsDirField.getText().trim();
|
||||
if (!cur.isBlank()) {
|
||||
File dir = new File(cur);
|
||||
if (dir.isDirectory()) dc.setInitialDirectory(dir);
|
||||
}
|
||||
File chosen = dc.showDialog(getDialogPane().getScene().getWindow());
|
||||
if (chosen != null) {
|
||||
modelsDirField.setText(chosen.getAbsolutePath());
|
||||
refreshModels();
|
||||
savePrefs();
|
||||
}
|
||||
}
|
||||
|
||||
/** Scannt das Modell-Verzeichnis nach .onnx-Dateien und befüllt die ComboBox. */
|
||||
private void refreshModels() {
|
||||
String dirStr = modelsDirField.getText().trim();
|
||||
if (dirStr.isBlank()) return;
|
||||
Path dir = Path.of(dirStr);
|
||||
if (!Files.isDirectory(dir)) return;
|
||||
|
||||
String prevSelection = modelCombo.getValue();
|
||||
modelCombo.getItems().clear();
|
||||
|
||||
try (Stream<Path> s = Files.list(dir)) {
|
||||
s.filter(p -> p.getFileName().toString().endsWith(".onnx")
|
||||
&& !p.getFileName().toString().endsWith(".onnx.1"))
|
||||
.sorted()
|
||||
.map(Path::toAbsolutePath)
|
||||
.map(Path::toString)
|
||||
.forEach(modelCombo.getItems()::add);
|
||||
} catch (IOException ignored) {}
|
||||
|
||||
// Zeige nur Dateinamen, interner Wert bleibt voller Pfad
|
||||
modelCombo.setButtonCell(new ModelCell());
|
||||
modelCombo.setCellFactory(lv -> new ModelCell());
|
||||
|
||||
if (prevSelection != null && modelCombo.getItems().contains(prevSelection)) {
|
||||
modelCombo.setValue(prevSelection);
|
||||
} else if (!modelCombo.getItems().isEmpty()) {
|
||||
modelCombo.setValue(modelCombo.getItems().get(0));
|
||||
}
|
||||
}
|
||||
|
||||
private static class ModelCell extends ListCell<String> {
|
||||
@Override
|
||||
protected void updateItem(String fullPath, boolean empty) {
|
||||
super.updateItem(fullPath, empty);
|
||||
if (empty || fullPath == null) { setText(null); return; }
|
||||
setText(Path.of(fullPath).getFileName().toString()
|
||||
.replaceAll("\\.onnx$", ""));
|
||||
}
|
||||
}
|
||||
|
||||
// ── Generierung ───────────────────────────────────────────────────────────
|
||||
|
||||
private void startGeneration() {
|
||||
String piperCmd = piperField.getText().trim();
|
||||
String modelPath = modelCombo.getValue();
|
||||
boolean heroMode = this.npcId.isBlank();
|
||||
boolean overwrite = overwriteCheck.isSelected();
|
||||
|
||||
if (piperCmd.isBlank()) { appendLog("FEHLER: Piper-Binary nicht angegeben."); return; }
|
||||
if (modelPath == null || modelPath.isBlank()) { appendLog("FEHLER: Kein Modell gewählt."); return; }
|
||||
if (!new File(modelPath).exists()) { appendLog("FEHLER: Modell nicht gefunden:\n " + modelPath); return; }
|
||||
|
||||
generateBtn.setDisable(true);
|
||||
logArea.clear();
|
||||
progressBar.setProgress(ProgressBar.INDETERMINATE_PROGRESS);
|
||||
|
||||
final String piperFinal = piperCmd, modelFinal = modelPath;
|
||||
Task<Void> task = new Task<>() {
|
||||
@Override protected Void call() {
|
||||
try {
|
||||
doGenerate(piperFinal, modelFinal, heroMode, overwrite);
|
||||
} catch (Exception e) {
|
||||
Platform.runLater(() -> {
|
||||
appendLog("FEHLER: " + e.getMessage());
|
||||
progressBar.setProgress(0);
|
||||
generateBtn.setDisable(false);
|
||||
});
|
||||
}
|
||||
return null;
|
||||
}
|
||||
};
|
||||
new Thread(task, "tts-generator").start();
|
||||
}
|
||||
|
||||
private void doGenerate(String piperCmd, String modelPath,
|
||||
boolean heroMode, boolean overwrite) throws IOException {
|
||||
|
||||
Map<String, String> texts = loadTexts();
|
||||
boolean hasFfmpeg = checkFfmpeg();
|
||||
Platform.runLater(() -> appendLog(hasFfmpeg
|
||||
? "[i] ffmpeg verfügbar — Ausgabe als OGG."
|
||||
: "[i] ffmpeg nicht gefunden — Ausgabe als WAV.\n"));
|
||||
|
||||
List<String> keys = heroMode ? collectHeroKeys() : collectNpcKeys();
|
||||
Platform.runLater(() -> appendLog(keys.size() + " Schlüssel zu verarbeiten.\n"));
|
||||
|
||||
Path assetsRoot = ProjectRoot.resolve("blight-assets", "src", "main", "resources");
|
||||
Path langDir = ProjectRoot.resolve("blight-lang", "src", "main", "resources", "lang");
|
||||
AudioBundle bundle = AudioBundleIO.loadOrEmpty("de", langDir);
|
||||
Map<String, String> audioEntries = new LinkedHashMap<>(bundle.getEntries());
|
||||
|
||||
int total = keys.size(), done = 0;
|
||||
|
||||
for (String key : keys) {
|
||||
String text = texts.getOrDefault(key, "").trim();
|
||||
if (text.isBlank()) {
|
||||
Platform.runLater(() -> appendLog("SKIP (kein Text): " + key));
|
||||
done++;
|
||||
final int d = done;
|
||||
Platform.runLater(() -> progressBar.setProgress((double) d / total));
|
||||
continue;
|
||||
}
|
||||
|
||||
String basePath = audioPathForKey(key);
|
||||
if (basePath.isBlank()) { done++; continue; }
|
||||
String relPath = hasFfmpeg ? basePath : basePath.replaceAll("\\.ogg$", ".wav");
|
||||
Path outPath = assetsRoot.resolve(relPath);
|
||||
|
||||
if (!overwrite && Files.exists(outPath)) {
|
||||
Platform.runLater(() -> appendLog("SKIP (vorhanden): " + relPath));
|
||||
done++;
|
||||
final int d = done;
|
||||
Platform.runLater(() -> progressBar.setProgress((double) d / total));
|
||||
continue;
|
||||
}
|
||||
|
||||
Files.createDirectories(outPath.getParent());
|
||||
boolean ok = hasFfmpeg
|
||||
? generateOgg(piperCmd, modelPath, text, outPath)
|
||||
: generateWav(piperCmd, modelPath, text, outPath);
|
||||
|
||||
if (ok) {
|
||||
audioEntries.put(key, relPath);
|
||||
Platform.runLater(() -> appendLog("OK: " + relPath));
|
||||
} else {
|
||||
Platform.runLater(() -> appendLog("FEHLER: " + relPath));
|
||||
}
|
||||
done++;
|
||||
final int d = done;
|
||||
Platform.runLater(() -> progressBar.setProgress((double) d / total));
|
||||
}
|
||||
|
||||
bundle.setEntries(audioEntries);
|
||||
AudioBundleIO.save(bundle, langDir);
|
||||
|
||||
Platform.runLater(() -> {
|
||||
progressBar.setProgress(1.0);
|
||||
appendLog("\n--- Fertig! audio_de.properties aktualisiert. ---");
|
||||
generateBtn.setDisable(false);
|
||||
});
|
||||
}
|
||||
|
||||
// ── Schlüssel sammeln ─────────────────────────────────────────────────────
|
||||
|
||||
private List<String> collectNpcKeys() {
|
||||
List<String> keys = new ArrayList<>();
|
||||
for (Map.Entry<String, DialogOption> e : npcOptions.entrySet()) {
|
||||
String base = "dialog." + npcId + "." + e.getKey();
|
||||
keys.addAll(DialogEditorView.resolveStepKeys(
|
||||
e.getValue().getNpcSteps(), e.getValue().getTextNpc(), base, ".textnpc"));
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
private List<String> collectHeroKeys() {
|
||||
List<String> keys = new ArrayList<>();
|
||||
Path charDir = ProjectRoot.resolve("blight-assets", "src", "main", "resources", "character");
|
||||
for (GameCharacter gc : CharacterIO.loadAll(charDir)) {
|
||||
if (!(gc instanceof NPC npc) || npc.getDialogOptions() == null) continue;
|
||||
for (Map.Entry<String, DialogOption> e : npc.getDialogOptions().entrySet()) {
|
||||
String base = "dialog." + npc.getCharacterId() + "." + e.getKey();
|
||||
keys.addAll(DialogEditorView.resolveStepKeys(
|
||||
e.getValue().getHeroSteps(), e.getValue().getTextHero(), base, ".textmainchar"));
|
||||
}
|
||||
}
|
||||
for (Monologue m : MonologueIO.load()) {
|
||||
if (m.getHeroSteps() == null) continue;
|
||||
m.getHeroSteps().stream()
|
||||
.filter(s -> s.getText() != null && !s.getText().id().isBlank())
|
||||
.map(s -> s.getText().id())
|
||||
.forEach(keys::add);
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
|
||||
// ── Piper / ffmpeg ────────────────────────────────────────────────────────
|
||||
|
||||
private boolean generateWav(String piperCmd, String modelPath, String text, Path out) {
|
||||
try {
|
||||
ProcessBuilder pb = new ProcessBuilder(
|
||||
piperCmd, "--model", modelPath, "--output_file", out.toString(), "--quiet");
|
||||
pb.redirectErrorStream(true);
|
||||
Process p = pb.start();
|
||||
try (OutputStream os = p.getOutputStream()) {
|
||||
os.write((text + "\n").getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
return p.waitFor() == 0;
|
||||
} catch (Exception e) { return false; }
|
||||
}
|
||||
|
||||
private boolean generateOgg(String piperCmd, String modelPath, String text, Path out) {
|
||||
Path tmp = out.resolveSibling(out.getFileName() + ".tmp.wav");
|
||||
try {
|
||||
if (!generateWav(piperCmd, modelPath, text, tmp)) return false;
|
||||
ProcessBuilder pb = new ProcessBuilder(
|
||||
"ffmpeg", "-i", tmp.toString(),
|
||||
"-ar", "22050", "-ac", "1", "-q:a", "4",
|
||||
out.toString(), "-y", "-loglevel", "error");
|
||||
pb.redirectErrorStream(true);
|
||||
return pb.start().waitFor() == 0;
|
||||
} catch (Exception e) { return false; }
|
||||
finally { try { Files.deleteIfExists(tmp); } catch (Exception ignored) {} }
|
||||
}
|
||||
|
||||
private boolean checkFfmpeg() {
|
||||
try {
|
||||
return new ProcessBuilder("ffmpeg", "-version")
|
||||
.redirectErrorStream(true).start().waitFor() == 0;
|
||||
} catch (Exception e) { return false; }
|
||||
}
|
||||
|
||||
// ── Hilfsmethoden ─────────────────────────────────────────────────────────
|
||||
|
||||
private static Map<String, String> loadTexts() {
|
||||
try {
|
||||
Path f = ProjectRoot.resolve(
|
||||
"blight-lang", "src", "main", "resources", "lang", "messages_de.properties");
|
||||
if (Files.exists(f)) return new LinkedHashMap<>(TextBundleIO.load(f).getEntries());
|
||||
} catch (Exception ignored) {}
|
||||
return Map.of();
|
||||
}
|
||||
|
||||
private static String audioPathForKey(String key) {
|
||||
if (key == null) return "";
|
||||
if (key.startsWith("dialog.")) {
|
||||
String w = key.substring("dialog.".length());
|
||||
int dot = w.indexOf('.');
|
||||
if (dot < 0) return "";
|
||||
return "audio/dialog/" + w.substring(0, dot) + "/"
|
||||
+ w.substring(dot + 1).replace('.', '_') + ".ogg";
|
||||
}
|
||||
if (key.startsWith("monologue.")) {
|
||||
String w = key.substring("monologue.".length());
|
||||
int dot = w.indexOf('.');
|
||||
if (dot < 0) return "audio/monologue/" + w + ".ogg";
|
||||
return "audio/monologue/" + w.substring(0, dot) + "_"
|
||||
+ w.substring(dot + 1).replace('.', '_') + ".ogg";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
private void appendLog(String msg) { logArea.appendText(msg + "\n"); }
|
||||
|
||||
private void loadPrefs() {
|
||||
try {
|
||||
if (!Files.exists(PREFS_FILE)) return;
|
||||
Properties p = new Properties();
|
||||
try (InputStream is = Files.newInputStream(PREFS_FILE)) { p.load(is); }
|
||||
piperField.setText(p.getProperty("piper.path", "piper"));
|
||||
modelsDirField.setText(p.getProperty("piper.models.dir", ""));
|
||||
refreshModels();
|
||||
String savedModel = p.getProperty("piper.model", "");
|
||||
if (!savedModel.isBlank() && modelCombo.getItems().contains(savedModel)) {
|
||||
modelCombo.setValue(savedModel);
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
private void savePrefs() {
|
||||
try {
|
||||
Properties p = new Properties();
|
||||
p.setProperty("piper.path", piperField.getText().trim());
|
||||
p.setProperty("piper.models.dir", modelsDirField.getText().trim());
|
||||
String sel = modelCombo.getValue();
|
||||
if (sel != null) p.setProperty("piper.model", sel);
|
||||
try (OutputStream os = Files.newOutputStream(PREFS_FILE)) {
|
||||
p.store(os, "Piper TTS Settings");
|
||||
}
|
||||
} catch (Exception ignored) {}
|
||||
}
|
||||
|
||||
private static HBox row(String labelText, javafx.scene.Node ctrl) {
|
||||
Label lbl = new Label(labelText);
|
||||
lbl.setMinWidth(140);
|
||||
HBox.setHgrow(ctrl, Priority.ALWAYS);
|
||||
HBox box = new HBox(8, lbl, ctrl);
|
||||
box.setAlignment(Pos.CENTER_LEFT);
|
||||
return box;
|
||||
}
|
||||
|
||||
private static Label bold(String text) {
|
||||
Label l = new Label(text);
|
||||
l.setStyle("-fx-font-weight: bold;");
|
||||
return l;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user