Ein Embed je Hof, das sich selbst aktualisiert
Der Fuhrpark stand bisher nur als Gesamtzahl im Karten-Embed. Jetzt bekommt jeder Hof eine eigene Nachricht im selben Kanal, die bei jedem Durchlauf bearbeitet statt neu gepostet wird: Land, Fahrzeuge mit Wert und Betriebsstunden, was in die Werkstatt muss, was gewaschen gehoert und was geladen ist. Die Fahrzeuge kommen aus vehicles.xml, blockweise gelesen — Verschleiss und Schmutz stehen in Kindelementen und gehoeren sonst dem falschen Fahrzeug. Gemietete Missionsfahrzeuge bleiben draussen, sie verfaelschen Anzahl und Wert. Faellt die Datei einmal aus, bleiben die Embeds stehen, statt dass ein Hof verschwindet und gleich darauf doppelt gepostet wird. Damit tauchen auch Hoefe auf, die noch kein Land gekauft haben: die Liste der Hoefe kommt aus Parzellen und Fuhrpark zusammen. Dazu raus, was nur den Kanal vollgeschrieben hat: die Meldung zum Monatswechsel und die zum Landkauf. Der Monat wird still weitergestellt — er steht ohnehin im Embed. Damit faellt der Ereignis-Kanal weg und mit ihm die gespeicherte Besitzliste. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+196
-68
@@ -15,7 +15,7 @@ import { brandEmbed } from '../embeds.js';
|
||||
import { brandFooter } from '../runtime-settings.js';
|
||||
import { moduleEnabled } from '../modules.js';
|
||||
import { tuning, tuningMs, everyTuned } from '../tuning.js';
|
||||
import { getSetting, setSetting, listGameservers, lsState, setLsMessage, setLsBesitz, setLsTageszeit } from '../db.js';
|
||||
import { getSetting, setSetting, listGameservers, lsState, setLsMessage, setLsTageszeit, setLsHofMessages } from '../db.js';
|
||||
|
||||
/** Alle Attribute eines XML-Tags als Objekt */
|
||||
const attrsVon = (roh) =>
|
||||
@@ -111,6 +111,62 @@ export function hofUebersicht(parzellen) {
|
||||
};
|
||||
}
|
||||
|
||||
// ── Fuhrpark ──────────────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Die Fahrzeuge aus der vehicles.xml.
|
||||
*
|
||||
* Blockweise statt attributweise: Verschleiss und Schmutz stehen in
|
||||
* Kindelementen, und wer nur nach `damage="…"` sucht, ordnet sie irgendeinem
|
||||
* Fahrzeug zu.
|
||||
*
|
||||
* Was NICHT drinsteht: die Tankgroesse. Die Datei nennt Liter, nicht Prozent —
|
||||
* „276 l Diesel" geht, „38 % voll" nicht.
|
||||
*/
|
||||
export function fahrzeugeLesen(xml) {
|
||||
return [...String(xml ?? '').matchAll(/<vehicle ([^>]*)>([\s\S]*?)<\/vehicle>/g)].map(([, kopf, rumpf]) => {
|
||||
const a = attrsVon(kopf);
|
||||
return {
|
||||
// aus data/vehicles/newHolland/t7/t7.xml wird "t7"
|
||||
modell: (a.filename ?? '').split('/').pop()?.replace(/\.xml$/, '') || '?',
|
||||
hof: Number(a.farmId) || 0,
|
||||
preis: Number(a.price) || 0,
|
||||
alter: Number(a.age) || 0,
|
||||
// operatingTime kommt in Sekunden
|
||||
stunden: (Number(a.operatingTime) || 0) / 3600,
|
||||
eigen: a.propertyState === 'OWNED',
|
||||
mod: a.modName || null,
|
||||
verschleiss: Number(rumpf.match(/<wearable damage="([\d.]+)"/)?.[1] ?? 0),
|
||||
dreck: Number(rumpf.match(/<dirtNode amount="([\d.]+)"/)?.[1] ?? 0),
|
||||
ladung: [...rumpf.matchAll(/<unit [^>]*fillType="(\w+)"[^>]*fillLevel="([\d.]+)"/g)]
|
||||
.map((m) => ({ art: m[1], menge: Number(m[2]) }))
|
||||
.filter((f) => f.menge > 0.5),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// Ab hier lohnt der Blick in die Werkstatt beziehungsweise die Waschanlage
|
||||
const VERSCHLEISS_AB = 0.3;
|
||||
const DRECK_AB = 0.7;
|
||||
|
||||
/** Fahrzeuge eines Hofes, zusammengefasst */
|
||||
export function fuhrpark(fahrzeuge, hof) {
|
||||
// Missionsfahrzeuge gehoeren nicht dem Hof — sie tauchen nur auf, solange
|
||||
// der Auftrag laeuft, und wuerden Wert und Anzahl verfaelschen.
|
||||
// Die Fahrzeugdatei wird einzeln geholt und darf einzeln ausfallen: dann
|
||||
// gibt es eben keinen Fuhrpark statt gar keinem Durchlauf.
|
||||
const meine = (fahrzeuge ?? []).filter((v) => v.eigen && v.hof === hof);
|
||||
return {
|
||||
anzahl: meine.length,
|
||||
wert: meine.reduce((s, v) => s + v.preis, 0),
|
||||
stunden: meine.reduce((s, v) => s + v.stunden, 0),
|
||||
werkstatt: meine.filter((v) => v.verschleiss >= VERSCHLEISS_AB)
|
||||
.sort((a, b) => b.verschleiss - a.verschleiss),
|
||||
waschen: meine.filter((v) => v.dreck >= DRECK_AB).sort((a, b) => b.dreck - a.dreck),
|
||||
fahrzeuge: meine,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Preise ────────────────────────────────────────────────────────────────
|
||||
|
||||
export const PERIODEN = [
|
||||
@@ -353,6 +409,89 @@ export function hofEmbed(client, stand, namen = {}) {
|
||||
return embed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ein Embed je Hof: Land, Fuhrpark und was ansteht.
|
||||
*
|
||||
* Getrennt statt alles in einem, weil jeder Hof seinen eigenen Leuten gehört —
|
||||
* und weil ein Embed mit fünf Höfen darin niemandem hilft, der nur wissen
|
||||
* will, ob sein Schlepper in die Werkstatt muss.
|
||||
*/
|
||||
export function hofDetailEmbed(client, hof, stand, fahrzeuge, namen = {}) {
|
||||
const wer = namen[hof] ?? `Hof ${hof}`;
|
||||
const land = hofUebersicht(stand.parzellen).hoefe.find((h) => h.hof === hof);
|
||||
const fp = fuhrpark(fahrzeuge, hof);
|
||||
const proz = (x) => `${Math.round(x * 100)} %`;
|
||||
|
||||
const embed = brandEmbed(client, 'HOF')
|
||||
.setColor(parseInt(hofFarbe(hof).slice(1), 16))
|
||||
.setTitle(`🚜 ${wer}`)
|
||||
.setFooter({ text: `${brandFooter('HOF')}${stand.karte ? ` • ${stand.karte}` : ''}` })
|
||||
.setTimestamp();
|
||||
|
||||
embed.addFields(
|
||||
{
|
||||
name: 'Land',
|
||||
value: land
|
||||
? `${land.parzellen} Parzellen · **${land.hektar.toFixed(1)} ha**\n${geld(land.wert)}`
|
||||
: 'noch kein Land gekauft',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Fuhrpark',
|
||||
value: fp.anzahl > 0
|
||||
? `**${fp.anzahl}** Fahrzeuge\n${geld(fp.wert)}`
|
||||
: 'noch keine Fahrzeuge',
|
||||
inline: true,
|
||||
},
|
||||
{
|
||||
name: 'Betriebsstunden',
|
||||
value: fp.anzahl > 0 ? `${fp.stunden.toFixed(1)} h` : '—',
|
||||
inline: true,
|
||||
},
|
||||
);
|
||||
|
||||
if (fp.werkstatt.length > 0) {
|
||||
embed.addFields({
|
||||
name: `🔧 In die Werkstatt (${fp.werkstatt.length})`,
|
||||
value: fp.werkstatt.slice(0, 8)
|
||||
.map((v) => `\`${v.modell}\` ${proz(v.verschleiss)}`).join(' · ').slice(0, 1000),
|
||||
});
|
||||
}
|
||||
if (fp.waschen.length > 0) {
|
||||
embed.addFields({
|
||||
name: `🧽 Waschen (${fp.waschen.length})`,
|
||||
value: fp.waschen.slice(0, 8)
|
||||
.map((v) => `\`${v.modell}\` ${proz(v.dreck)}`).join(' · ').slice(0, 1000),
|
||||
});
|
||||
}
|
||||
|
||||
// Was in den Fahrzeugen dieses Hofes liegt — ohne Betriebsstoffe
|
||||
const ladung = new Map();
|
||||
for (const v of fp.fahrzeuge) {
|
||||
for (const l of v.ladung) {
|
||||
if (['DIESEL', 'DEF', 'AIR', 'ELECTRICCHARGE'].includes(l.art)) continue;
|
||||
ladung.set(l.art, (ladung.get(l.art) ?? 0) + l.menge);
|
||||
}
|
||||
}
|
||||
if (ladung.size > 0) {
|
||||
embed.addFields({
|
||||
name: 'Auf Lager',
|
||||
value: [...ladung.entries()].sort((a, b) => b[1] - a[1]).slice(0, 8)
|
||||
.map(([art, menge]) => `**${sortenName(art)}** ${Math.round(menge).toLocaleString('de-DE')} l`)
|
||||
.join(' · ').slice(0, 1000),
|
||||
});
|
||||
}
|
||||
return embed;
|
||||
}
|
||||
|
||||
/** Welche Höfe gibt es überhaupt? Aus Land und Fuhrpark zusammen. */
|
||||
export function hoefeImSpiel(stand, fahrzeuge) {
|
||||
const ids = new Set();
|
||||
for (const p of stand?.parzellen ?? []) if (p.hof > 0) ids.add(p.hof);
|
||||
for (const v of fahrzeuge ?? []) if (v.eigen && v.hof > 0) ids.add(v.hof);
|
||||
return [...ids].sort((a, b) => a - b);
|
||||
}
|
||||
|
||||
/** Karte + Embed als fertige Nachricht */
|
||||
export async function hofNachricht(client, stand, bildB64, namen = {}, breite = 1024) {
|
||||
const embed = hofEmbed(client, stand, namen);
|
||||
@@ -402,8 +541,10 @@ export const SORTEN_NAMEN = {
|
||||
OILRADISH: 'Ölrettich', STRAW: 'Stroh', CHAFF: 'Häcksel',
|
||||
SILAGE: 'Silage', FORAGE: 'Mischration', HAY: 'Heu',
|
||||
// Tierisches
|
||||
MILK: 'Milch', WOOL: 'Wolle', EGG: 'Eier', HONEY: 'Honig',
|
||||
MILK: 'Milch', GOATMILK: 'Ziegenmilch', BUFFALOMILK: 'Büffelmilch',
|
||||
WOOL: 'Wolle', EGG: 'Eier', HONEY: 'Honig',
|
||||
MANURE: 'Mist', LIQUIDMANURE: 'Gülle', DIGESTATE: 'Gärrest',
|
||||
COMPOST: 'Kompost', MINERAL_FEED: 'Mineralfutter', PIGFOOD: 'Schweinefutter',
|
||||
// Betriebsstoffe
|
||||
SEEDS: 'Saatgut', FERTILIZER: 'Dünger', LIQUIDFERTILIZER: 'Flüssigdünger',
|
||||
HERBICIDE: 'Herbizid', LIME: 'Kalk', WATER: 'Wasser',
|
||||
@@ -511,42 +652,39 @@ export async function lsAbfragen(server, { timeoutMs = 15000, kartenBreite = 102
|
||||
const stats = await holen(`${basis}/dedicated-server-stats.xml?code=${code}`, timeoutMs);
|
||||
const stand = statsLesen(stats);
|
||||
|
||||
// Karte und Preise sind Beiwerk: fehlen sie, bleibt der Rest brauchbar
|
||||
const [bild, preise] = await Promise.all([
|
||||
// Karte, Preise und Fuhrpark sind Beiwerk: fehlt eins, bleibt der Rest
|
||||
// brauchbar. Deshalb einzeln abgefangen statt einmal um alles herum.
|
||||
const [bild, preise, fahrzeuge] = await Promise.all([
|
||||
holen(`${basis}/dedicated-server-stats-map.jpg?code=${code}&quality=85&size=${kartenBreite}`, timeoutMs, false)
|
||||
.then((b) => b.toString('base64')).catch(() => null),
|
||||
holen(`${basis}/dedicated-server-savegame.html?file=economy&code=${code}`, timeoutMs)
|
||||
.then(preiseLesen).catch(() => []),
|
||||
// null heisst „nicht bekommen", [] heisst „wirklich keine Fahrzeuge".
|
||||
// Der Unterschied zählt: bei einem Ausfall dürfen die Hof-Embeds nicht
|
||||
// angefasst werden, sonst verschwindet ein Hof und wird neu gepostet.
|
||||
holen(`${basis}/dedicated-server-savegame.html?file=vehicles&code=${code}`, timeoutMs)
|
||||
.then(fahrzeugeLesen).catch(() => null),
|
||||
]);
|
||||
return { stand, bild, preise };
|
||||
return { stand, bild, preise, fahrzeuge };
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Was hat sich am Besitz geändert?
|
||||
* Vorhandene Nachricht bearbeiten, sonst eine neue schicken.
|
||||
*
|
||||
* Nur Übergänge melden, bei denen wirklich jemand gekauft oder verkauft hat.
|
||||
* Beim allerersten Durchlauf gibt es nichts zu melden — sonst käme beim Start
|
||||
* des Bots eine Meldung pro bereits gekaufter Parzelle.
|
||||
* Gibt die ID zurück, wenn eine neue entstanden ist — beim Bearbeiten `null`,
|
||||
* dann bleibt der Merker, wie er ist.
|
||||
*/
|
||||
export function besitzDiff(vorher, nachher) {
|
||||
if (!vorher) return [];
|
||||
const alt = new Map(vorher.map((p) => [p.id, p.hof]));
|
||||
const raus = [];
|
||||
for (const p of nachher) {
|
||||
const vor = alt.get(p.id);
|
||||
if (vor === undefined || vor === p.hof) continue;
|
||||
raus.push({ parzelle: p, von: vor, nach: p.hof });
|
||||
async function pflegen(kanal, id, beimBearbeiten, beimSenden) {
|
||||
if (id) {
|
||||
const vorhanden = await kanal.messages.fetch(id).catch(() => null);
|
||||
if (vorhanden) {
|
||||
await vorhanden.edit(beimBearbeiten);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return raus;
|
||||
}
|
||||
|
||||
/** Kauf-Meldung: "Hof 2 hat Parzelle 47 gekauft (2,3 ha)" */
|
||||
function wechselText(w, namen) {
|
||||
const wer = (h) => (h === 0 ? null : namen[h] ?? `Hof ${h}`);
|
||||
const gross = `${w.parzelle.hektar.toFixed(1)} ha`;
|
||||
if (w.von === 0) return `🟢 **${wer(w.nach)}** hat Parzelle **${w.parzelle.name}** gekauft — ${gross}, ${geld(w.parzelle.preis)}`;
|
||||
if (w.nach === 0) return `🔴 **${wer(w.von)}** hat Parzelle **${w.parzelle.name}** verkauft — ${gross}`;
|
||||
return `🔁 Parzelle **${w.parzelle.name}** ging von **${wer(w.von)}** an **${wer(w.nach)}** — ${gross}`;
|
||||
const neu = await kanal.send(beimSenden);
|
||||
return neu.id;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -564,44 +702,24 @@ export async function lsTick(client) {
|
||||
|
||||
const channelId = getSetting('ls_channel_id');
|
||||
const kanal = channelId ? await client.channels.fetch(channelId).catch(() => null) : null;
|
||||
const alarmId = getSetting('ls_event_channel_id') || channelId;
|
||||
const alarm = alarmId ? await client.channels.fetch(alarmId).catch(() => null) : null;
|
||||
const breite = Number(tuning('ls_map_size')) || 1024;
|
||||
|
||||
for (const s of server) {
|
||||
try {
|
||||
const { stand, bild, preise } = await lsAbfragen(s, {
|
||||
const { stand, bild, preise, fahrzeuge } = await lsAbfragen(s, {
|
||||
timeoutMs: tuningMs.seconds('monitor_timeout'),
|
||||
kartenBreite: breite,
|
||||
});
|
||||
const namen = hofNamen(getSetting('ls_farm_names'));
|
||||
|
||||
// Besitzwechsel melden, bevor der neue Stand gespeichert wird
|
||||
const alt = lsState(s.id);
|
||||
const wechsel = besitzDiff(alt?.besitz, stand.parzellen);
|
||||
if (wechsel.length > 0 && alarm?.isTextBased()) {
|
||||
await alarm.send({
|
||||
embeds: [brandEmbed(client, 'LAND')
|
||||
.setTitle('🚜 Land hat den Besitzer gewechselt')
|
||||
.setDescription(wechsel.slice(0, 15).map((w) => wechselText(w, namen)).join('\n'))],
|
||||
}).catch(() => {});
|
||||
}
|
||||
setLsBesitz(s.id, stand.parzellen.map((p) => ({ id: p.id, hof: p.hof })));
|
||||
|
||||
// Neuer Spieltag = neuer Monat. Nur weiterstellen, wenn überhaupt
|
||||
// eine Jahreszeit eingetragen ist — ohne Ausgangspunkt liesse sich
|
||||
// nur raten, und ein geratener Monat ist schlimmer als keiner.
|
||||
const gesetzt = periodeLesen(getSetting('ls_period'));
|
||||
// Neuer Spieltag = neuer Monat. Still weitergestellt, nicht
|
||||
// gemeldet: die Preisliste braucht den Monat, aber eine Nachricht
|
||||
// dafür wäre einmal am Tag Lärm für eine Zahl, die ohnehin im
|
||||
// Embed steht.
|
||||
if (tagUmgeschlagen(alt?.tageszeit, stand.tageszeit)) {
|
||||
const naechste = naechstePeriode(gesetzt);
|
||||
if (naechste) {
|
||||
setSetting('ls_period', naechste);
|
||||
await alarm?.send({
|
||||
embeds: [brandEmbed(client, 'ZEIT')
|
||||
.setTitle('📅 Neuer Monat auf dem Hof')
|
||||
.setDescription(`Jetzt **${PERIODE_LABEL[naechste]}** — die Preise haben sich verschoben.`)],
|
||||
}).catch(() => {});
|
||||
}
|
||||
const naechste = naechstePeriode(getSetting('ls_period'));
|
||||
if (naechste) setSetting('ls_period', naechste);
|
||||
}
|
||||
// Eine Antwort ohne Uhrzeit darf den letzten guten Stand nicht
|
||||
// überschreiben — sonst gilt beim nächsten Durchlauf „nichts
|
||||
@@ -609,27 +727,37 @@ export async function lsTick(client) {
|
||||
if (stand.tageszeit != null) setLsTageszeit(s.id, stand.tageszeit);
|
||||
|
||||
if (!kanal?.isTextBased()) continue;
|
||||
|
||||
// 1) Karte und Preise
|
||||
const payload = await hofNachricht(client, stand, bild, namen, breite);
|
||||
// Preise ändern sich nur mit der Jahreszeit — ein zweites Embed in
|
||||
// derselben Nachricht spart einen Dauerposten im Kanal.
|
||||
if (preise.length > 0) {
|
||||
payload.embeds.push(preisEmbed(client, preise, {
|
||||
jetzt: periodeLesen(getSetting('ls_period')), // nach dem Weiterstellen neu gelesen
|
||||
sorten: String(getSetting('ls_crops') ?? '').split(',').map((s) => s.trim()).filter(Boolean),
|
||||
sorten: String(getSetting('ls_crops') ?? '').split(',').map((x) => x.trim()).filter(Boolean),
|
||||
}));
|
||||
}
|
||||
const alteId = alt?.message_id;
|
||||
if (alteId) {
|
||||
const vorhanden = await kanal.messages.fetch(alteId).catch(() => null);
|
||||
if (vorhanden) {
|
||||
// Beim Bearbeiten muss der alte Anhang weg, sonst hängen
|
||||
// zwei Karten an der Nachricht und Discord zeigt die alte.
|
||||
await vorhanden.edit({ ...payload, attachments: [] });
|
||||
continue;
|
||||
}
|
||||
// Beim Bearbeiten muss der alte Anhang weg, sonst hängen zwei
|
||||
// Karten an der Nachricht und Discord zeigt die alte.
|
||||
const karteId = await pflegen(kanal, alt?.message_id, { ...payload, attachments: [] }, payload);
|
||||
if (karteId) setLsMessage(s.id, karteId);
|
||||
|
||||
// 2) Ein Embed je Hof, ebenfalls bearbeitet statt neu gepostet.
|
||||
// Kam die Fahrzeugdatei nicht an, bleibt alles stehen, wie es ist:
|
||||
// lieber einen Durchlauf lang alte Zahlen als ein Doppelpost.
|
||||
if (fahrzeuge == null) continue;
|
||||
const hofIds = hoefeImSpiel(stand, fahrzeuge);
|
||||
const merker = { ...(alt?.hofMessages ?? {}) };
|
||||
for (const hof of hofIds) {
|
||||
const inhalt = { embeds: [hofDetailEmbed(client, hof, stand, fahrzeuge, namen)] };
|
||||
const id = await pflegen(kanal, merker[hof], inhalt, inhalt);
|
||||
if (id) merker[hof] = id;
|
||||
}
|
||||
const neu = await kanal.send(payload);
|
||||
setLsMessage(s.id, neu.id);
|
||||
// Höfe, die es nicht mehr gibt, aus dem Merker werfen — sonst
|
||||
// wächst er mit jedem gelöschten Hof weiter.
|
||||
for (const hof of Object.keys(merker)) {
|
||||
if (!hofIds.includes(Number(hof))) delete merker[hof];
|
||||
}
|
||||
setLsHofMessages(s.id, merker);
|
||||
} catch (error) {
|
||||
console.error(`[ls-farm] ${s.name}:`, error.message);
|
||||
}
|
||||
|
||||
@@ -1023,7 +1023,6 @@ db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS ls_state (
|
||||
server_id INTEGER PRIMARY KEY,
|
||||
message_id TEXT,
|
||||
besitz TEXT,
|
||||
geprueft TEXT NOT NULL DEFAULT (datetime('now'))
|
||||
);
|
||||
`);
|
||||
@@ -1032,10 +1031,6 @@ const lsMessageStmt = db.prepare(`
|
||||
INSERT INTO ls_state (server_id, message_id) VALUES (?, ?)
|
||||
ON CONFLICT(server_id) DO UPDATE SET message_id = excluded.message_id
|
||||
`);
|
||||
const lsBesitzStmt = db.prepare(`
|
||||
INSERT INTO ls_state (server_id, besitz, geprueft) VALUES (?, ?, datetime('now'))
|
||||
ON CONFLICT(server_id) DO UPDATE SET besitz = excluded.besitz, geprueft = datetime('now')
|
||||
`);
|
||||
// Letzte gesehene Tageszeit — daran erkennt das Modul den Tageswechsel
|
||||
{
|
||||
const cols = db.prepare('PRAGMA table_info(ls_state)').all().map((c) => c.name);
|
||||
@@ -1046,18 +1041,31 @@ const lsTagStmt = db.prepare(`
|
||||
ON CONFLICT(server_id) DO UPDATE SET tageszeit = excluded.tageszeit
|
||||
`);
|
||||
export const setLsTageszeit = (serverId, ms) => lsTagStmt.run(serverId, ms);
|
||||
|
||||
// Eine gepflegte Nachricht je Hof: { "1": "messageId", "2": … }
|
||||
{
|
||||
const cols = db.prepare('PRAGMA table_info(ls_state)').all().map((c) => c.name);
|
||||
if (!cols.includes('hof_messages')) db.exec(`ALTER TABLE ls_state ADD COLUMN hof_messages TEXT NOT NULL DEFAULT '{}'`);
|
||||
}
|
||||
const lsHofStmt = db.prepare(`
|
||||
INSERT INTO ls_state (server_id, hof_messages) VALUES (?, ?)
|
||||
ON CONFLICT(server_id) DO UPDATE SET hof_messages = excluded.hof_messages
|
||||
`);
|
||||
export const setLsHofMessages = (serverId, map) => lsHofStmt.run(serverId, JSON.stringify(map));
|
||||
const lsVergessenStmt = db.prepare('DELETE FROM ls_state WHERE server_id = ?');
|
||||
export const lsState = (serverId) => {
|
||||
const r = lsStateStmt.get(serverId);
|
||||
if (!r) return null;
|
||||
// Kaputtes JSON darf den Durchlauf nicht anhalten — dann gilt eben
|
||||
// "noch nichts bekannt" und beim nächsten Mal steht wieder etwas drin.
|
||||
let besitz = null;
|
||||
try { besitz = r.besitz ? JSON.parse(r.besitz) : null; } catch { besitz = null; }
|
||||
return { ...r, besitz };
|
||||
let hofMessages = {};
|
||||
try {
|
||||
const roh = JSON.parse(r.hof_messages || '{}');
|
||||
if (roh && typeof roh === 'object' && !Array.isArray(roh)) hofMessages = roh;
|
||||
} catch { /* kaputt gespeichert — dann werden die Embeds neu gepostet */ }
|
||||
return { ...r, hofMessages };
|
||||
};
|
||||
export const setLsMessage = (serverId, messageId) => lsMessageStmt.run(serverId, messageId);
|
||||
export const setLsBesitz = (serverId, besitz) => lsBesitzStmt.run(serverId, JSON.stringify(besitz));
|
||||
export const forgetLsState = (serverId) => lsVergessenStmt.run(serverId).changes > 0;
|
||||
// Erreichbarkeit stundenweise zusammengefasst — für den Uptime-Balken.
|
||||
// Die Rohdaten (alle 2 min) wären über eine Woche rund 5000 Zeilen pro Server;
|
||||
|
||||
+2
-3
@@ -248,12 +248,11 @@ export const MODULES = [
|
||||
},
|
||||
{
|
||||
id: 'ls_farm', group: 'gaming', name: 'LS — Höfe & Preise', nav: true,
|
||||
desc: 'Landwirtschafts-Simulator: Besitzkarte mit Höfen, Fahrzeugen und Spielern, '
|
||||
+ 'dazu die Preiskurven übers Jahr. Braucht einen LS-Server mit Zugangs-Code unter „Game-Server".',
|
||||
desc: 'Landwirtschafts-Simulator: Besitzkarte, Preiskurven übers Jahr und je Hof ein '
|
||||
+ 'Embed mit Land, Fuhrpark und was in die Werkstatt muss. Braucht einen LS-Server mit Zugangs-Code unter „Game-Server".',
|
||||
tab: 'server', setting: 'ls_farm_enabled',
|
||||
requires: [{ setting: 'ls_channel_id', label: 'Karten-Kanal', create: { name: 'ls-hoefe', topic: 'Besitzkarte und Preise vom LS-Server' } }],
|
||||
fields: [
|
||||
{ setting: 'ls_event_channel_id', label: 'Ereignis-Kanal', art: 'kanal', hint: 'Land gekauft oder verkauft; leer = in den Karten-Kanal' },
|
||||
{ setting: 'ls_farm_names', label: 'Hof-Namen', art: 'text', hint: 'der Feed kennt nur Nummern — z. B. „1=Alexander, 2=Timo, 3=Kitty"' },
|
||||
{ setting: 'ls_period', label: 'Aktueller Monat', art: 'text', hint: 'z. B. „September" oder „9" — steht nicht im Feed, deshalb einmal eintragen. Danach zählt der Bot bei jedem Tageswechsel selbst weiter. Leer heißt: Kurve ohne „jetzt verkaufen"-Rat.' },
|
||||
{ setting: 'ls_crops', label: 'Sorten in der Preisliste', art: 'text', hint: 'kommagetrennt, leer = die üblichen. Ohne Auswahl verdrängt Spargel jede echte Feldfrucht.' },
|
||||
|
||||
Reference in New Issue
Block a user