diff --git a/modDesc.xml b/modDesc.xml index 7bb62fe..994ccec 100644 --- a/modDesc.xml +++ b/modDesc.xml @@ -1,7 +1,7 @@ SimGamerJen, Hyper138 - 2.0.3.5 + 2.0.3.6 <en>Crop Control Override</en> <de>Crop Control Override Öffnen</de> @@ -35,67 +35,17 @@ Configuration: - A crop weight of 0 excludes that crop from NPC-field reseeding without disabling it for the player. - The global leaveCultivated weight remains available, allowing weighted resets to leave some fields cultivated instead of reseeding them. -Changelog 2.0.3.5: +Changelog 2.0.3.6: - Replaced the seasonalMission and seasonalLifecycle category weights with an individual 0-5 reseed weight for every crop. - Added GUI controls for editing the reseed weight of the selected crop. - A crop weight of 0 now excludes that crop from NPC-field reseeding while leaving the crop otherwise enabled. - Retained the global leaveCultivated weighting. - Added automatic migration of existing per-save XML files, assigning the default weight of 5 where reseedWeight is missing and removing obsolete category weights when files are normalised. - Added multiplayer synchronisation for per-crop reseed weights. - -Changelog 2.0.3.4: -- Disabled player crops are now removed immediately from the selectable seed list of loaded, newly purchased, and newly loaded sowing machines. -- Re-enabling a crop restores it to compatible seeders without requiring a savegame reload. -- Added server-authoritative sowing and direct-sowing safeguards so stale selections, helpers, multiplayer clients, and compatible third-party worker systems cannot plant prohibited crops. -- Active AI fieldwork is stopped when a disabled crop is selected, the sowing implement is switched off, and a centre-screen Crop Control Override warning explains why seeding is not permitted. -- Improved multiplayer crop-rule delivery with a server push after client loading, client retry handling, and deferred client-side policy application until the authoritative server snapshot arrives. -- Added and completed French localisation support. - -Changelog 2.0.1.9: -- Expanded external l10n support so GUI and gameplay text can be translated through language files. -- Added German and French language-file support and prepared the mod for community-contributed translations. - -Changelog 2.0.1.8: -- Rebuilt cached sowing-machine seed lists whenever crop rules change, so disabled crops disappear from seed selectors immediately. -- Added a live sowing guard and filtering for newly loaded or purchased seeders. -- Improved dedicated-server rule synchronisation with a server-side push and client retry process. -- Prevented remote clients from applying temporary all-enabled placeholder rules while waiting for the server snapshot. -- Reset multiplayer and original fruit-state data fully when leaving a mission. -- Fixed config normalisation and template-backup handling for custom reseed settings. - -Changelog 2.0.1.7: -- Fixed the permission-hook installer load order so the admin-permission build loads cleanly before installing Farm Management admin listeners. - -Changelog 2.0.1.6: -- Remote dedicated-server clients that elevate to game admin/master user through the Farm Management admin console can now unlock CCO editing for that session. -- Added a permission refresh handshake so admin status changes are sent to the server and the CCO GUI refreshes without using local client XML files. -- Server-side validation still protects rule changes; normal remote clients remain read-only. - -Changelog 2.0.1.5: -- Added dedicated-server admin/master-user edit permissions using the EasyDevControls-style authority model. -- Remote server admins can edit CCO rules through server-validated multiplayer events while normal remote clients remain read-only. -- Dedicated-server clients still do not read, create, or write local CCO XML files. - -Changelog 2.0.1.4: -- Fixed the custom CCO GUI opening path by removing the unstable paging selector that could trigger a TextElement stack overflow in some FS25 builds. -- Remote multiplayer clients now show a waiting/sync state instead of rendering local default rules before the server snapshot arrives. - -Changelog 2.0.1.3: -- Fixed remote multiplayer client sync requests firing before the server connection is ready. - -Changelog 2.0.1.2: -- Dedicated/cloud multiplayer clients now ignore local CCO XML files and use an in-memory ruleset synced from the server. -- Remote multiplayer clients are read-only by default, preventing local savegame XML creation or accidental rule writes. -- Single-player, local-host multiplayer, and server-side file behaviour remain unchanged. - -Changelog 2.0.1.1: -- Fixed dedicated/cloud multiplayer client GUI refresh after server-side rule changes; the originating client now receives the server-authoritative update and refreshes an open CCO GUI. - -Changelog 2.0.1.0: -- Added multiplayer event handling so GUI changes and server-side reset/apply actions are sent to the server on dedicated multiplayer games. -- Updated NPC crop planning to use FieldManager.generatePlannedFruitForField instead of the old mission-generation flag mutation. -- Added defensive error handling around NPC crop replacement during planned fruit generation. -- Moved localisation to external l10n files so additional languages can be added more easily. +- Fixed dedicated-server player joins changing loaded seeder selections, commonly back to Wheat. +- Prevented remote CCO seed-list refreshes from sending seed-selection changes back to the authoritative server. +- Hardened NPC crop planning so a failed replacement does not leave SowMission with an invalid fruit reference. +- Added recovery for legacy invalid sow contracts that can trigger repeated getIsPlantableInPeriod errors. Hotkeys: ALT+C opens the Crop Control Override GUI. This keybind can be changed in the in-game controls menu. @@ -134,67 +84,17 @@ Konfiguration: - Eine Gewichtung von 0 schließt die Fruchtart von der Nachsaat auf NPC-Feldern aus, ohne sie für den Spieler zu deaktivieren. - Die globale Gewichtung leaveCultivated bleibt erhalten, sodass ein gewichteter Reset einige Felder im kultivierten Zustand belassen kann. -Änderungen 2.0.3.5: +Änderungen 2.0.3.6: - Die Kategoriegewichtungen seasonalMission und seasonalLifecycle wurden durch eine individuelle Nachsaatgewichtung von 0 bis 5 für jede Fruchtart ersetzt. - GUI-Steuerelemente zum Bearbeiten der Nachsaatgewichtung der ausgewählten Fruchtart hinzugefügt. - Eine Fruchtart mit Gewichtung 0 wird nun von der Nachsaat auf NPC-Feldern ausgeschlossen, bleibt aber ansonsten aktiviert. - Die globale Gewichtung leaveCultivated wurde beibehalten. - Bestehende Spielstand-XML-Dateien werden automatisch migriert: Fehlt reseedWeight, wird der Standardwert 5 gesetzt; veraltete Kategoriegewichtungen werden bei der Normalisierung entfernt. - Multiplayer-Synchronisation für individuelle Nachsaatgewichtungen hinzugefügt. - -Änderungen 2.0.3.4: -- Für Spieler deaktivierte Fruchtarten werden nun sofort aus der auswählbaren Saatgutliste bereits geladener, neu gekaufter und neu geladener Sämaschinen entfernt. -- Wird eine Fruchtart wieder aktiviert, erscheint sie ohne Neuladen des Spielstands erneut in kompatiblen Sämaschinen. -- Serverseitig verbindliche Schutzprüfungen für normale und direkte Aussaat verhindern, dass veraltete Auswahlen, Helfer, Multiplayer-Clients oder kompatible Drittanbieter-Helfersysteme gesperrte Fruchtarten säen. -- Aktive KI-Feldarbeit wird beendet, wenn eine deaktivierte Fruchtart ausgewählt ist; die Sämaschine wird ausgeschaltet und eine zentrale Crop-Control-Override-Warnung erklärt, warum die Aussaat nicht erlaubt ist. -- Die Übertragung der Multiplayer-Regeln wurde durch einen Server-Push nach dem Client-Ladevorgang, Wiederholungsversuche des Clients und eine verzögerte clientseitige Anwendung bis zum Eingang des verbindlichen Server-Snapshots verbessert. -- Französische Lokalisierung hinzugefügt und vervollständigt. - -Änderungen 2.0.1.9: -- Die externe l10n-Unterstützung wurde erweitert, damit GUI- und Spieltexte über Sprachdateien übersetzt werden können. -- Unterstützung für deutsche und französische Sprachdateien hinzugefügt und der Mod für Community-Übersetzungen vorbereitet. - -Änderungen 2.0.1.8: -- Zwischengespeicherte Saatgutlisten von Sämaschinen werden bei Regeländerungen neu aufgebaut, sodass deaktivierte Fruchtarten sofort aus der Auswahl verschwinden. -- Eine aktive Aussaat-Schutzprüfung sowie Filterung für neu geladene oder gekaufte Sämaschinen wurden hinzugefügt. -- Die Regel-Synchronisation auf dedizierten Servern wurde durch Server-Push und Wiederholungsversuche des Clients verbessert. -- Entfernte Clients wenden während des Wartens auf den Server-Snapshot keine vorübergehend vollständig aktivierten Platzhalterregeln mehr an. -- Multiplayer-Status und ursprüngliche Fruchtartenwerte werden beim Verlassen einer Mission vollständig zurückgesetzt. -- Fehler bei der Konfigurationsnormalisierung und bei Vorlagen-Sicherungen für benutzerdefinierte Nachsaat-Einstellungen wurden behoben. - -Änderungen 2.0.1.7: -- Die Lade-Reihenfolge des Berechtigungs-Hook-Installers korrigiert, damit der Admin-Berechtigungs-Build sauber lädt, bevor Listener für Farmverwaltungs-Administratoren installiert werden. - -Änderungen 2.0.1.6: -- Entfernte Clients auf dedizierten Servern, die sich über die Farmverwaltung als Spieladministrator/Master User anmelden, können CCO-Bearbeitung nun für diese Sitzung freischalten. -- Eine Berechtigungsaktualisierung sendet Admin-Statusänderungen an den Server und aktualisiert die CCO-GUI, ohne lokale Client-XML-Dateien zu verwenden. -- Die serverseitige Validierung schützt Regeländerungen weiterhin; normale entfernte Clients bleiben schreibgeschützt. - -Änderungen 2.0.1.5: -- Admin-/Master-User-Bearbeitungsrechte für dedizierte Server nach dem EasyDevControls-Berechtigungsmodell hinzugefügt. -- Entfernte Serveradministratoren können CCO-Regeln über serverseitig validierte Mehrspieler-Ereignisse bearbeiten, während normale entfernte Clients schreibgeschützt bleiben. -- Clients auf dedizierten Servern lesen, erstellen oder schreiben weiterhin keine lokalen CCO-XML-Dateien. - -Änderungen 2.0.1.4: -- Korrigiert den CCO-GUI-Start, indem der instabile Seitenwahlschalter entfernt wurde, der in manchen FS25-Versionen einen TextElement-Stack-Overflow auslösen konnte. -- Entfernte Mehrspieler-Clients zeigen nun einen Warte-/Synchronisationsstatus, statt lokale Standardregeln vor dem Server-Snapshot darzustellen. - -Änderungen 2.0.1.3: -- Synchronisationsanfragen entfernter Multiplayer-Clients werden nun erst gesendet, wenn die Serververbindung bereit ist. - -Änderungen 2.0.1.2: -- Clients im dedizierten/Cloud-Multiplayer ignorieren lokale CCO-XML-Dateien und verwenden einen vom Server synchronisierten Regelsatz im Speicher. -- Entfernte Multiplayer-Clients sind standardmäßig schreibgeschützt, wodurch lokale savegame-XML-Erstellung und unbeabsichtigte Regeländerungen verhindert werden. -- Einzelspieler-, lokaler Host-Multiplayer- und serverseitiges Dateiverhalten bleiben unverändert. - -Änderungen 2.0.1.1: -- Client-GUI-Aktualisierung im dedizierten/Cloud-Multiplayer nach serverseitigen Regeländerungen korrigiert; der auslösende Client erhält nun die serverseitige Aktualisierung und aktualisiert eine geöffnete CCO-GUI. - -Änderungen 2.0.1.0: -- Multiplayer-Ereignisverarbeitung hinzugefügt, damit GUI-Änderungen und serverseitige Zurücksetzen-/Anwenden-Aktionen in dedizierten Multiplayer-Spielen an den Server gesendet werden. -- Die NPC-Fruchtplanung verwendet nun FieldManager.generatePlannedFruitForField anstelle der bisherigen Änderung von Missions-Flags. -- Defensive Fehlerbehandlung für die NPC-Ersatzfruchtwahl hinzugefügt. -- Lokalisierung in externe l10n-Dateien verschoben, damit weitere Sprachen einfacher ergänzt werden können. +- Behoben, dass der Beitritt eines Spielers zu einem dedizierten Server die Saatgutauswahl geladener Sämaschinen ändern konnte, häufig zurück auf Weizen. +- Verhindert, dass CCO-Aktualisierungen auf entfernten Clients Änderungen der Saatgutauswahl an den autoritativen Server zurücksenden. +- NPC-Fruchtplanung gehärtet, damit eine fehlgeschlagene Ersatzauswahl keinen ungültigen Fruchtverweis in SowMission hinterlässt. +- Wiederherstellung für ältere ungültige Säaufträge hinzugefügt, die wiederholte getIsPlantableInPeriod-Fehler auslösen können. Tastenkombinationen: ALT+C öffnet die Crop Control Override GUI. Diese Tastenkombination kann im Steuerungsmenü des Spiels geändert werden. @@ -233,67 +133,17 @@ Configuration : - Une pondération de 0 exclut la culture du ressemis des champs PNJ sans la désactiver pour le joueur. - La pondération globale leaveCultivated est conservée, ce qui permet à une réinitialisation pondérée de laisser certains champs cultivés sans les ressemer. -Journal des modifications 2.0.3.5 : +Journal des modifications 2.0.3.6 : - Remplacement des pondérations de catégorie seasonalMission et seasonalLifecycle par une pondération de ressemis individuelle de 0 à 5 pour chaque culture. - Ajout de commandes dans l’interface pour modifier la pondération de ressemis de la culture sélectionnée. - Une culture avec une pondération de 0 est désormais exclue du ressemis des champs PNJ tout en restant activée pour les autres usages. - Conservation de la pondération globale leaveCultivated. - Migration automatique des fichiers XML de sauvegarde existants : la valeur par défaut 5 est attribuée lorsque reseedWeight est absent et les anciennes pondérations de catégorie sont supprimées lors de la normalisation. - Ajout de la synchronisation multijoueur des pondérations de ressemis par culture. - -Journal des modifications 2.0.3.4 : -- Les cultures désactivées pour le joueur sont désormais retirées immédiatement de la liste de semences des semoirs déjà chargés, nouvellement achetés ou nouvellement chargés. -- Lorsqu’une culture est réactivée, elle réapparaît dans les semoirs compatibles sans nécessiter le rechargement de la sauvegarde. -- Des protections autoritaires côté serveur pour le semis normal et le semis direct empêchent les anciennes sélections, les ouvriers, les clients multijoueurs et les systèmes d’ouvriers tiers compatibles de semer des cultures interdites. -- Le travail de champ d’un ouvrier IA est arrêté lorsqu’une culture désactivée est sélectionnée, le semoir est éteint et un avertissement central Crop Control Override explique pourquoi le semis n’est pas autorisé. -- La synchronisation multijoueur des règles a été améliorée grâce à un envoi serveur après le chargement du client, à des tentatives répétées côté client et au report de l’application locale jusqu’à la réception de l’instantané serveur faisant autorité. -- Ajout et finalisation de la localisation française. - -Journal des modifications 2.0.1.9 : -- Extension de la prise en charge l10n externe afin que les textes de l’interface et du jeu puissent être traduits via des fichiers de langue. -- Ajout de la prise en charge des fichiers allemands et français et préparation du mod aux traductions fournies par la communauté. - -Journal des modifications 2.0.1.8 : -- Reconstruction des listes de semences mises en cache par les semoirs à chaque modification des règles, afin que les cultures désactivées disparaissent immédiatement du sélecteur. -- Ajout d’une protection active pendant le semis et du filtrage des semoirs nouvellement chargés ou achetés. -- Amélioration de la synchronisation des règles sur serveur dédié avec un envoi serveur et des tentatives répétées côté client. -- Les clients distants n’appliquent plus de règles temporaires entièrement activées en attendant l’instantané serveur. -- Réinitialisation complète de l’état multijoueur et des valeurs d’origine des cultures à la sortie d’une mission. -- Correction de la normalisation de la configuration et de la sauvegarde des modèles pour les paramètres personnalisés de réensemencement. - -Journal des modifications 2.0.1.7 : -- Correction de l’ordre de chargement de l’installation des hooks de permissions afin que la version avec permissions administrateur se charge correctement avant d’installer les écouteurs d’administration de la gestion de ferme. - -Journal des modifications 2.0.1.6 : -- Les clients distants d’un serveur dédié qui élèvent leurs droits en administrateur/master user via la gestion de ferme peuvent désormais déverrouiller l’édition CCO pour la session. -- Ajout d’une actualisation des permissions afin d’envoyer les changements de statut administrateur au serveur et de rafraîchir l’interface CCO sans utiliser de fichiers XML locaux côté client. -- La validation côté serveur protège toujours les changements de règles ; les clients distants normaux restent en lecture seule. - -Journal des modifications 2.0.1.5 : -- Ajout de la détection des droits d’édition administrateur/master user sur serveur dédié, inspirée du modèle de permissions d’EasyDevControls. -- Les administrateurs distants du serveur peuvent désormais modifier les règles CCO via des événements multijoueurs validés par le serveur, tandis que les clients distants normaux restent en lecture seule. -- Les clients dédiés ne lisent, ne créent et n’écrivent toujours aucun fichier XML CCO local. - -Journal des modifications 2.0.1.4 : -- Correction de l’ouverture de l’interface CCO en supprimant le sélecteur de pages instable qui pouvait provoquer un dépassement de pile TextElement dans certaines versions de FS25. -- Les clients multijoueurs distants affichent désormais un état d’attente/synchronisation au lieu d’afficher les règles locales par défaut avant la réception de l’instantané serveur. - -Journal des modifications 2.0.1.3 : -- Correction des demandes de synchronisation des clients multijoueurs distants déclenchées avant que la connexion serveur soit prête. - -Journal des modifications 2.0.1.2 : -- Les clients en multijoueur dédié/cloud ignorent désormais les fichiers XML CCO locaux et utilisent un jeu de règles en mémoire synchronisé depuis le serveur. -- Les clients multijoueurs distants sont en lecture seule par défaut, empêchant la création de XML savegame locaux ou les écritures de règles accidentelles. -- Le comportement fichier en solo, hôte multijoueur local et côté serveur reste inchangé. - -Journal des modifications 2.0.1.1 : -- Correction de l’actualisation de l’interface client en multijoueur dédié/cloud après les changements de règles côté serveur ; le client à l’origine de la demande reçoit désormais la mise à jour serveur et actualise l’interface CCO ouverte. - -Journal des modifications 2.0.1.0 : -- Ajout de la gestion des événements multijoueur afin que les changements de l’interface et les actions serveur d’application/réinitialisation soient envoyés au serveur dans les parties multijoueur dédiées. -- La planification des cultures PNJ utilise désormais FieldManager.generatePlannedFruitForField au lieu de modifier temporairement les indicateurs de missions. -- Ajout d’une gestion défensive des erreurs lors du choix d’une culture PNJ de remplacement. -- Déplacement de la localisation vers des fichiers l10n externes afin de faciliter l’ajout d’autres langues. +- Correction d’un problème où l’arrivée d’un joueur sur un serveur dédié pouvait modifier la culture sélectionnée dans les semoirs chargés, souvent vers le blé. +- Les actualisations CCO sur les clients distants n’envoient plus de changement de sélection de semence au serveur faisant autorité. +- Renforcement de la planification des cultures PNJ afin qu’un échec de remplacement ne laisse pas SowMission avec une référence de culture invalide. +- Ajout d’une récupération pour les anciens contrats de semis invalides pouvant provoquer des erreurs répétées getIsPlantableInPeriod. Raccourcis : ALT+C ouvre l’interface Crop Control Override. Ce raccourci peut être modifié dans le menu des commandes du jeu. diff --git a/scripts/CropControlOverride.lua b/scripts/CropControlOverride.lua index 023cd8e..fc462f9 100644 --- a/scripts/CropControlOverride.lua +++ b/scripts/CropControlOverride.lua @@ -13,7 +13,7 @@ CropControlOverride = { MOD_ID = g_currentModName or "FS25_CropControlOverride", - VERSION = "2.0.3.5", + VERSION = "2.0.3.6", _origFlags = {}, _rules = {}, @@ -33,6 +33,7 @@ CropControlOverride = { _permissionHooksApplied = false, _clientReportedMasterUser = false, _seedGuardHooksApplied = false, + _sowUpdateGuardApplied = false, _serverSettingsRetryTimer = 0, _serverSettingsRetryCount = 0, } @@ -553,7 +554,7 @@ function CCO:_applyNpcBlockedFlags(fruit) -- doing so can cause contract-list flicker and affects player-facing crop data. end -function CCO:applyRules(rules) +function CCO:applyRules(rules, seedSyncMode) if g_fruitTypeManager == nil then return end rules = rules or self._rules or {} @@ -581,7 +582,8 @@ function CCO:applyRules(rules) -- machines that are already loaded cached their seed list at vehicle load -- time, so a mid-session rule change (GUI APPLY, server sync on a remote -- client) never reached them. Rebuild those cached lists now. - local ok, e = pcall(function() self:refreshAllSowingMachines() end) + local preserveNetworkIndex = seedSyncMode == "serverSnapshot" and isClientOnlyMultiplayer() + local ok, e = pcall(function() self:refreshAllSowingMachines(preserveNetworkIndex) end) if not ok then debug("refreshAllSowingMachines skipped: " .. tostring(e)) end end @@ -1061,7 +1063,7 @@ function CCO:getVehicleSelectedSeedFruitIndex(vehicle) return nil end -function CCO:filterSowingMachineSeeds(vehicle) +function CCO:filterSowingMachineSeeds(vehicle, preserveNetworkIndex) local spec = getSowingMachineSpec(vehicle) if spec == nil or type(spec.seeds) ~= "table" then return false end @@ -1081,12 +1083,9 @@ function CCO:filterSowingMachineSeeds(vehicle) end local filtered = {} - local removed = 0 for _, fruitIndex in ipairs(spec.ccoOriginalSeeds) do if self:isFruitIndexAllowedForPlayer(fruitIndex) then table.insert(filtered, fruitIndex) - else - removed = removed + 1 end end @@ -1096,29 +1095,74 @@ function CCO:filterSowingMachineSeeds(vehicle) filtered = spec.ccoOriginalSeeds end + -- Multiplayer rule snapshots are frequently re-applied when clients join. + -- A no-op policy sync must not rebuild the list or touch the live seed index. + local listUnchanged = (#filtered == #spec.seeds) + if listUnchanged then + for i, fruitIndex in ipairs(filtered) do + if spec.seeds[i] ~= fruitIndex then + listUnchanged = false + break + end + end + end + if listUnchanged then + return false + end + -- Replace contents in-place so any engine references to the table survive. for i = #spec.seeds, 1, -1 do spec.seeds[i] = nil end for i, fruitIndex in ipairs(filtered) do spec.seeds[i] = fruitIndex end - -- Keep the current selection valid; move to the same fruit if it survived, - -- otherwise snap to the first allowed entry. - local newIndex = 1 - if previousSelected ~= nil then - for i, fruitIndex in ipairs(spec.seeds) do - if fruitIndex == previousSelected then newIndex = i break end + local newIndex = nil + + if preserveNetworkIndex == true and selIndex ~= nil then + -- On a remote client's server snapshot, currentSeed/seedIndex is an + -- authoritative network slot. Before CCO filtering, the client's stale + -- seed list can map that slot to the wrong fruit, so preserve the slot + -- itself rather than deriving a crop from the pre-filter list. + newIndex = math.min(math.max(1, math.floor(selIndex)), #spec.seeds) + else + -- For authoritative local/server rule changes, keep the same fruit if + -- it remains permitted even when filtering moves it to another slot. + if previousSelected ~= nil then + for i, fruitIndex in ipairs(spec.seeds) do + if fruitIndex == previousSelected then + newIndex = i + break + end + end + end + + if newIndex == nil then + newIndex = 1 + if previousSelected ~= nil then + warn(("seed selection for %s was reset to slot 1 because its previous crop is no longer permitted here") + :format(vehicle.getName ~= nil and vehicle:getName() or tostring(vehicle))) + end end end - if spec.currentSeed ~= nil then - if vehicle.setSeedIndex ~= nil then - pcall(function() vehicle:setSeedIndex(newIndex) end) + + -- If the numeric slot is unchanged, rebuilding the list is sufficient. + if selIndex ~= nil and selIndex == newIndex then + return true + end + + if vehicle.setSeedIndex ~= nil then + if isClientOnlyMultiplayer() then + -- Never let a remote client's CCO cache refresh send a native + -- SetSeedIndexEvent back to the authoritative server. + pcall(function() vehicle:setSeedIndex(newIndex, true) end) else - spec.currentSeed = math.min(math.max(1, newIndex), #spec.seeds) + pcall(function() vehicle:setSeedIndex(newIndex) end) end + elseif spec.currentSeed ~= nil then + spec.currentSeed = math.min(math.max(1, newIndex), #spec.seeds) elseif spec.seedIndex ~= nil then spec.seedIndex = math.min(math.max(1, newIndex), #spec.seeds) end - return removed > 0 + return true end local function iterMissionVehicles() @@ -1132,11 +1176,11 @@ local function iterMissionVehicles() return vehicles or {} end -function CCO:refreshAllSowingMachines() +function CCO:refreshAllSowingMachines(preserveNetworkIndex) local refreshed = 0 for _, vehicle in pairs(iterMissionVehicles()) do if getSowingMachineSpec(vehicle) ~= nil then - local ok, changed = pcall(function() return self:filterSowingMachineSeeds(vehicle) end) + local ok, changed = pcall(function() return self:filterSowingMachineSeeds(vehicle, preserveNetworkIndex) end) if ok and changed == true then refreshed = refreshed + 1 end end end @@ -1146,7 +1190,6 @@ function CCO:refreshAllSowingMachines() return refreshed end - local CCO_SPECIAL_RESEED_EXCLUSIONS = { GRAPE = true, OLIVE = true, @@ -2278,9 +2321,9 @@ function CCO:applyRuntimeHooks() return replacement.index end - debug(("blocked NPC crop choice %s on field %s (%.2f ha): %s; %s"):format( - cropName, tostring(getFieldId(field)), fieldHa, tostring(reason), tostring(replacementReason))) - return nil + warn(("blocked NPC crop choice %s on field %s (%.2f ha): %s; retaining engine fruit index %s so mission data remains valid (%s)"):format( + cropName, tostring(getFieldId(field)), fieldHa, tostring(reason), tostring(fruitIndex), tostring(replacementReason))) + return fruitIndex end return fruitIndex @@ -2289,6 +2332,42 @@ function CCO:applyRuntimeHooks() end end +local function getSowMissionCropName(mission) + if mission == nil or g_fruitTypeManager == nil then return nil end + + local fruit = mission.fruitType + if type(fruit) == "table" then + return fruit.name + end + + local fruitIndex = tonumber(mission.fruitTypeIndex or mission.fruitIndex or fruit) + if fruitIndex == nil or (FruitType ~= nil and fruitIndex == FruitType.UNKNOWN) then return nil end + local descriptor = g_fruitTypeManager:getFruitTypeByIndex(fruitIndex) + return descriptor ~= nil and descriptor.name or nil +end + +function CCO:discardInvalidSowMission(mission, failure) + if mission == nil then return end + if mission._ccoInvalidFruitQueued ~= true then + mission._ccoInvalidFruitQueued = true + local field = mission.field + warn(("discarding invalid SowMission before repeated update failure field=%s fruitType=%s fruitTypeIndex=%s error=%s"):format( + tostring(field ~= nil and getFieldId(field) or "UNKNOWN"), + tostring(mission.fruitType), tostring(mission.fruitTypeIndex or mission.fruitIndex), tostring(failure))) + end + + if g_missionManager ~= nil and g_missionManager.markMissionForDeletion ~= nil then + g_missionManager:markMissionForDeletion(mission) + elseif mission.delete ~= nil then + local ok, deleteError = pcall(mission.delete, mission) + if not ok then warn("failed deleting invalid SowMission: " .. tostring(deleteError)) end + end + + if g_missionManager ~= nil and g_missionManager.generationTimer ~= nil then + g_missionManager.generationTimer = 0 + end +end + function CCO:applyLateHooks() if SowMission ~= nil and SowMission.isAvailableForField ~= nil and not self._sowHookApplied then self._sowHookApplied = true @@ -2297,15 +2376,7 @@ function CCO:applyLateHooks() local result = originalSowIsAvailable(field, mission, ...) if not result then return false end - local cropName = nil - if mission ~= nil then - if mission.fruitType ~= nil then - cropName = mission.fruitType.name - elseif mission.fruitTypeIndex ~= nil and g_fruitTypeManager ~= nil then - local ft = g_fruitTypeManager:getFruitTypeByIndex(mission.fruitTypeIndex) - if ft ~= nil then cropName = ft.name end - end - end + local cropName = getSowMissionCropName(mission) if cropName == nil then return result end @@ -2320,6 +2391,24 @@ function CCO:applyLateHooks() end debug("hooked SowMission.isAvailableForField") end + + if SowMission ~= nil and SowMission.update ~= nil and not self._sowUpdateGuardApplied then + self._sowUpdateGuardApplied = true + SowMission.update = Utils.overwrittenFunction(SowMission.update, function(mission, superFunc, dt) + local ok, result = pcall(superFunc, mission, dt) + if ok then return result end + + local failure = tostring(result) + if failure:find("attempt to index nil", 1, true) ~= nil + and failure:find("getIsPlantableInPeriod", 1, true) ~= nil then + CCO:discardInvalidSowMission(mission, failure) + return + end + + error(result, 0) + end) + debug("hooked SowMission.update (invalid fruit recovery guard)") + end end -- PDA / UI filtering hooks, inherited from the original CCO build. @@ -3230,7 +3319,7 @@ function CCO:applyServerSettingsPayload(payload, configPath, saveId, canEdit) -- applyRules also rebuilds the seed lists of already-loaded sowing -- machines, so a snapshot that arrives after vehicles loaded still takes -- effect immediately. - self:applyRules(self._rules) + self:applyRules(self._rules, "serverSnapshot") local msg = self._serverCanEditRules == true and "Server CCO rules synced. Admin editing is enabled for this session. Local CCO XML files were not read or written." or "Server CCO rules synced. Local CCO XML files were not read or written."