Add server authoritative fire spread state
This commit is contained in:
@@ -840,7 +840,7 @@ Target deliverable: A small group can join a server, spawn into one biome, gathe
|
|||||||
- [x] Add unattended and poorly maintained fire risk for campfires and other open-flame sources. Added server-side campfire risk state for lit duration, seconds since maintenance, cleared area, containment, high fuel, wet weather mitigation, and a replicated `FireRiskScore` that later ignition/spread systems can consume.
|
- [x] Add unattended and poorly maintained fire risk for campfires and other open-flame sources. Added server-side campfire risk state for lit duration, seconds since maintenance, cleared area, containment, high fuel, wet weather mitigation, and a replicated `FireRiskScore` that later ignition/spread systems can consume.
|
||||||
- [x] Add grass and forest ignition checks from irresponsible fire placement, wind/weather, dry fuel, nearby vegetation, and burn duration. Added foliage fuel counting, campfire vegetation ignition risk scores, grass/brush and forest ignition flags, and weather/wind/burn-duration modifiers so unsafe fire placement near dry fuel can now become a server-authoritative ignition risk.
|
- [x] Add grass and forest ignition checks from irresponsible fire placement, wind/weather, dry fuel, nearby vegetation, and burn duration. Added foliage fuel counting, campfire vegetation ignition risk scores, grass/brush and forest ignition flags, and weather/wind/burn-duration modifiers so unsafe fire placement near dry fuel can now become a server-authoritative ignition risk.
|
||||||
- [x] Add shelter/structure ignition risk when fires are placed too close to primitive shelters, wood piles, flammable crafting stations, or settlement objects. Added campfire structure ignition risk for nearby primitive shelters and flammable wood/fiber resource nodes, with containment, burn-duration, weather/wind, and fire-risk modifiers before a replicated structure ignition flag is set.
|
- [x] Add shelter/structure ignition risk when fires are placed too close to primitive shelters, wood piles, flammable crafting stations, or settlement objects. Added campfire structure ignition risk for nearby primitive shelters and flammable wood/fiber resource nodes, with containment, burn-duration, weather/wind, and fire-risk modifiers before a replicated structure ignition flag is set.
|
||||||
- [ ] Add server-authoritative fire spread rules for grass, brush, trees, shelters, and other burnable actors, including fuel, distance, wind, weather, and suppression hooks.
|
- [x] Add server-authoritative fire spread rules for grass, brush, trees, shelters, and other burnable actors, including fuel, distance, wind, weather, and suppression hooks. Added replicated grass, forest, and structure fire intensities plus active spread radius that grow only on the server from nearby fuel, ignition distance, wind/weather, and a suppression-pressure hook for later rain, carried water, dirt/sand, firebreaks, and tools.
|
||||||
- [ ] Add fire maintenance gameplay so watched, cleared, contained, or extinguished fires are safe, while neglected fires can become dangerous.
|
- [ ] Add fire maintenance gameplay so watched, cleared, contained, or extinguished fires are safe, while neglected fires can become dangerous.
|
||||||
- [ ] Add fire suppression hooks for rain, water carrying, dirt/sand, cleared firebreaks, and future firefighting tools.
|
- [ ] Add fire suppression hooks for rain, water carrying, dirt/sand, cleared firebreaks, and future firefighting tools.
|
||||||
- [ ] Persist active grass, forest, and structure fires across save/load without corrupting world state.
|
- [ ] Persist active grass, forest, and structure fires across save/load without corrupting world state.
|
||||||
|
|||||||
@@ -361,6 +361,14 @@ structure risk accumulates with distance-based fuel presence, burn duration,
|
|||||||
weather/wind, and the current fire-risk ratio before setting a replicated
|
weather/wind, and the current fire-risk ratio before setting a replicated
|
||||||
structure ignition flag.
|
structure ignition flag.
|
||||||
|
|
||||||
|
Active fire spread is still server-authoritative at the campfire/open-flame
|
||||||
|
source. Once grass/brush, forest, or structure ignition flags are set, the server
|
||||||
|
updates replicated fire intensities and an active spread radius. The spread rule
|
||||||
|
uses nearby fuel, ignition distance implied by the existing checks, wind/weather
|
||||||
|
modifiers, and a suppression-pressure hook that future rain, carried water,
|
||||||
|
dirt/sand, firebreaks, and tools can drive. This establishes deterministic fire
|
||||||
|
state for clients without letting clients decide whether the world is burning.
|
||||||
|
|
||||||
Campfires expose native extinguish logic through `AAgrarianCampfire::Extinguish`.
|
Campfires expose native extinguish logic through `AAgrarianCampfire::Extinguish`.
|
||||||
Extinguishing clears remaining fuel, turns off replicated lit state, and reuses
|
Extinguishing clears remaining fuel, turns off replicated lit state, and reuses
|
||||||
the same visual update path as natural fuel depletion.
|
the same visual update path as natural fuel depletion.
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Verify server-authoritative fire spread state exists."""
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
ROOT = Path(__file__).resolve().parents[1]
|
||||||
|
FIRE_H = ROOT / "Source" / "AgrarianGame" / "AgrarianCampfire.h"
|
||||||
|
FIRE_CPP = ROOT / "Source" / "AgrarianGame" / "AgrarianCampfire.cpp"
|
||||||
|
TDD = ROOT / "Docs" / "TechnicalDesignDocument.md"
|
||||||
|
ROADMAP = ROOT / "AGRARIAN_DEVELOPMENT_ROADMAP.md"
|
||||||
|
|
||||||
|
REQUIRED = {
|
||||||
|
FIRE_H: [
|
||||||
|
"GrassFireIntensity",
|
||||||
|
"ForestFireIntensity",
|
||||||
|
"StructureFireIntensity",
|
||||||
|
"ActiveFireSpreadRadius",
|
||||||
|
"BaseFireSpreadRadius",
|
||||||
|
"MaxFireSpreadRadius",
|
||||||
|
"FireSpreadIntensityPerSecond",
|
||||||
|
"FireSuppressionPressure",
|
||||||
|
"UpdateServerAuthoritativeFireSpread",
|
||||||
|
"GetFireSpreadWeatherMultiplier",
|
||||||
|
"GetActiveBurningFuelScore",
|
||||||
|
],
|
||||||
|
FIRE_CPP: [
|
||||||
|
"DOREPLIFETIME(AAgrarianCampfire, GrassFireIntensity)",
|
||||||
|
"DOREPLIFETIME(AAgrarianCampfire, ForestFireIntensity)",
|
||||||
|
"DOREPLIFETIME(AAgrarianCampfire, StructureFireIntensity)",
|
||||||
|
"DOREPLIFETIME(AAgrarianCampfire, ActiveFireSpreadRadius)",
|
||||||
|
"UpdateServerAuthoritativeFireSpread(DeltaSeconds);",
|
||||||
|
"AAgrarianCampfire::UpdateServerAuthoritativeFireSpread",
|
||||||
|
"if (!HasAuthority())",
|
||||||
|
"FireSuppressionPressure",
|
||||||
|
"GetFireSpreadWeatherMultiplier()",
|
||||||
|
"GetActiveBurningFuelScore()",
|
||||||
|
"ActiveFireSpreadRadius = FMath::Clamp",
|
||||||
|
"grass_fire_intensity",
|
||||||
|
"active_fire_spread_radius",
|
||||||
|
],
|
||||||
|
TDD: [
|
||||||
|
"Active fire spread is still server-authoritative",
|
||||||
|
"replicated fire intensities",
|
||||||
|
"active spread radius",
|
||||||
|
"suppression-pressure hook",
|
||||||
|
],
|
||||||
|
ROADMAP: [
|
||||||
|
"[x] Add server-authoritative fire spread rules",
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
missing = []
|
||||||
|
for path, snippets in REQUIRED.items():
|
||||||
|
text = path.read_text(encoding="utf-8")
|
||||||
|
for snippet in snippets:
|
||||||
|
if snippet not in text:
|
||||||
|
missing.append(f"{path.relative_to(ROOT)} missing {snippet!r}")
|
||||||
|
if missing:
|
||||||
|
raise SystemExit("FAILED: " + "; ".join(missing))
|
||||||
|
print("OK: server-authoritative fire spread state is implemented.")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -146,6 +146,7 @@ void AAgrarianCampfire::Tick(float DeltaSeconds)
|
|||||||
UpdateFireRisk(DeltaSeconds);
|
UpdateFireRisk(DeltaSeconds);
|
||||||
UpdateVegetationIgnitionRisk(DeltaSeconds);
|
UpdateVegetationIgnitionRisk(DeltaSeconds);
|
||||||
UpdateStructureIgnitionRisk(DeltaSeconds);
|
UpdateStructureIgnitionRisk(DeltaSeconds);
|
||||||
|
UpdateServerAuthoritativeFireSpread(DeltaSeconds);
|
||||||
WarmNearbyCharacters(DeltaSeconds);
|
WarmNearbyCharacters(DeltaSeconds);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -169,6 +170,10 @@ void AAgrarianCampfire::GetLifetimeReplicatedProps(TArray<FLifetimeProperty>& Ou
|
|||||||
DOREPLIFETIME(AAgrarianCampfire, bForestFuelIgnited);
|
DOREPLIFETIME(AAgrarianCampfire, bForestFuelIgnited);
|
||||||
DOREPLIFETIME(AAgrarianCampfire, StructureIgnitionRiskScore);
|
DOREPLIFETIME(AAgrarianCampfire, StructureIgnitionRiskScore);
|
||||||
DOREPLIFETIME(AAgrarianCampfire, bStructureIgnited);
|
DOREPLIFETIME(AAgrarianCampfire, bStructureIgnited);
|
||||||
|
DOREPLIFETIME(AAgrarianCampfire, GrassFireIntensity);
|
||||||
|
DOREPLIFETIME(AAgrarianCampfire, ForestFireIntensity);
|
||||||
|
DOREPLIFETIME(AAgrarianCampfire, StructureFireIntensity);
|
||||||
|
DOREPLIFETIME(AAgrarianCampfire, ActiveFireSpreadRadius);
|
||||||
}
|
}
|
||||||
|
|
||||||
FText AAgrarianCampfire::GetInteractionText_Implementation(const AAgrarianGameCharacter* Interactor) const
|
FText AAgrarianCampfire::GetInteractionText_Implementation(const AAgrarianGameCharacter* Interactor) const
|
||||||
@@ -218,6 +223,10 @@ void AAgrarianCampfire::CapturePersistentState_Implementation(UAgrarianPersisten
|
|||||||
PersistentComponent->NumberState.Add(TEXT("forest_fuel_ignited"), bForestFuelIgnited ? 1.0f : 0.0f);
|
PersistentComponent->NumberState.Add(TEXT("forest_fuel_ignited"), bForestFuelIgnited ? 1.0f : 0.0f);
|
||||||
PersistentComponent->NumberState.Add(TEXT("structure_ignition_risk_score"), StructureIgnitionRiskScore);
|
PersistentComponent->NumberState.Add(TEXT("structure_ignition_risk_score"), StructureIgnitionRiskScore);
|
||||||
PersistentComponent->NumberState.Add(TEXT("structure_ignited"), bStructureIgnited ? 1.0f : 0.0f);
|
PersistentComponent->NumberState.Add(TEXT("structure_ignited"), bStructureIgnited ? 1.0f : 0.0f);
|
||||||
|
PersistentComponent->NumberState.Add(TEXT("grass_fire_intensity"), GrassFireIntensity);
|
||||||
|
PersistentComponent->NumberState.Add(TEXT("forest_fire_intensity"), ForestFireIntensity);
|
||||||
|
PersistentComponent->NumberState.Add(TEXT("structure_fire_intensity"), StructureFireIntensity);
|
||||||
|
PersistentComponent->NumberState.Add(TEXT("active_fire_spread_radius"), ActiveFireSpreadRadius);
|
||||||
}
|
}
|
||||||
|
|
||||||
void AAgrarianCampfire::ApplyPersistentState_Implementation(UAgrarianPersistentActorComponent* PersistentComponent)
|
void AAgrarianCampfire::ApplyPersistentState_Implementation(UAgrarianPersistentActorComponent* PersistentComponent)
|
||||||
@@ -243,6 +252,10 @@ void AAgrarianCampfire::ApplyPersistentState_Implementation(UAgrarianPersistentA
|
|||||||
const float* SavedForestIgnited = PersistentComponent->NumberState.Find(TEXT("forest_fuel_ignited"));
|
const float* SavedForestIgnited = PersistentComponent->NumberState.Find(TEXT("forest_fuel_ignited"));
|
||||||
const float* SavedStructureIgnitionRisk = PersistentComponent->NumberState.Find(TEXT("structure_ignition_risk_score"));
|
const float* SavedStructureIgnitionRisk = PersistentComponent->NumberState.Find(TEXT("structure_ignition_risk_score"));
|
||||||
const float* SavedStructureIgnited = PersistentComponent->NumberState.Find(TEXT("structure_ignited"));
|
const float* SavedStructureIgnited = PersistentComponent->NumberState.Find(TEXT("structure_ignited"));
|
||||||
|
const float* SavedGrassFireIntensity = PersistentComponent->NumberState.Find(TEXT("grass_fire_intensity"));
|
||||||
|
const float* SavedForestFireIntensity = PersistentComponent->NumberState.Find(TEXT("forest_fire_intensity"));
|
||||||
|
const float* SavedStructureFireIntensity = PersistentComponent->NumberState.Find(TEXT("structure_fire_intensity"));
|
||||||
|
const float* SavedActiveFireSpreadRadius = PersistentComponent->NumberState.Find(TEXT("active_fire_spread_radius"));
|
||||||
|
|
||||||
if (SavedFuelSeconds)
|
if (SavedFuelSeconds)
|
||||||
{
|
{
|
||||||
@@ -319,6 +332,26 @@ void AAgrarianCampfire::ApplyPersistentState_Implementation(UAgrarianPersistentA
|
|||||||
bStructureIgnited = *SavedStructureIgnited > 0.5f;
|
bStructureIgnited = *SavedStructureIgnited > 0.5f;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (SavedGrassFireIntensity)
|
||||||
|
{
|
||||||
|
GrassFireIntensity = FMath::Clamp(*SavedGrassFireIntensity, 0.0f, 100.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SavedForestFireIntensity)
|
||||||
|
{
|
||||||
|
ForestFireIntensity = FMath::Clamp(*SavedForestFireIntensity, 0.0f, 100.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SavedStructureFireIntensity)
|
||||||
|
{
|
||||||
|
StructureFireIntensity = FMath::Clamp(*SavedStructureFireIntensity, 0.0f, 100.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SavedActiveFireSpreadRadius)
|
||||||
|
{
|
||||||
|
ActiveFireSpreadRadius = FMath::Clamp(*SavedActiveFireSpreadRadius, 0.0f, MaxFireSpreadRadius);
|
||||||
|
}
|
||||||
|
|
||||||
SetLit(SavedLit && *SavedLit > 0.5f && FuelSeconds > 0.0f);
|
SetLit(SavedLit && *SavedLit > 0.5f && FuelSeconds > 0.0f);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -347,6 +380,10 @@ void AAgrarianCampfire::Extinguish()
|
|||||||
GrassIgnitionRiskScore = 0.0f;
|
GrassIgnitionRiskScore = 0.0f;
|
||||||
ForestIgnitionRiskScore = 0.0f;
|
ForestIgnitionRiskScore = 0.0f;
|
||||||
StructureIgnitionRiskScore = 0.0f;
|
StructureIgnitionRiskScore = 0.0f;
|
||||||
|
GrassFireIntensity = 0.0f;
|
||||||
|
ForestFireIntensity = 0.0f;
|
||||||
|
StructureFireIntensity = 0.0f;
|
||||||
|
ActiveFireSpreadRadius = 0.0f;
|
||||||
LitDurationSeconds = 0.0f;
|
LitDurationSeconds = 0.0f;
|
||||||
SecondsSinceMaintenance = 0.0f;
|
SecondsSinceMaintenance = 0.0f;
|
||||||
SetLit(false);
|
SetLit(false);
|
||||||
@@ -728,3 +765,64 @@ float AAgrarianCampfire::GetStructureFuelScoreNearFire() const
|
|||||||
|
|
||||||
return StructureFuelScore;
|
return StructureFuelScore;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void AAgrarianCampfire::UpdateServerAuthoritativeFireSpread(float DeltaSeconds)
|
||||||
|
{
|
||||||
|
if (!HasAuthority())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const bool bAnyActiveFire = bGrassOrBrushIgnited || bForestFuelIgnited || bStructureIgnited;
|
||||||
|
if (!bAnyActiveFire)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const float WeatherMultiplier = GetFireSpreadWeatherMultiplier();
|
||||||
|
const float SuppressionMultiplier = FMath::Clamp(1.0f - FireSuppressionPressure, 0.0f, 1.0f);
|
||||||
|
const float FuelScore = FMath::Max(1.0f, GetActiveBurningFuelScore());
|
||||||
|
const float IntensityDelta = FireSpreadIntensityPerSecond * WeatherMultiplier * SuppressionMultiplier * FuelScore * DeltaSeconds;
|
||||||
|
|
||||||
|
if (bGrassOrBrushIgnited)
|
||||||
|
{
|
||||||
|
GrassFireIntensity = FMath::Clamp(GrassFireIntensity + IntensityDelta, 0.0f, 100.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bForestFuelIgnited)
|
||||||
|
{
|
||||||
|
ForestFireIntensity = FMath::Clamp(ForestFireIntensity + (IntensityDelta * 0.75f), 0.0f, 100.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (bStructureIgnited)
|
||||||
|
{
|
||||||
|
StructureFireIntensity = FMath::Clamp(StructureFireIntensity + (IntensityDelta * 0.9f), 0.0f, 100.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
const float TotalIntensity = GrassFireIntensity + ForestFireIntensity + StructureFireIntensity;
|
||||||
|
ActiveFireSpreadRadius = FMath::Clamp(
|
||||||
|
BaseFireSpreadRadius + (TotalIntensity * 12.0f * WeatherMultiplier),
|
||||||
|
0.0f,
|
||||||
|
MaxFireSpreadRadius);
|
||||||
|
}
|
||||||
|
|
||||||
|
float AAgrarianCampfire::GetFireSpreadWeatherMultiplier() const
|
||||||
|
{
|
||||||
|
float Multiplier = GetVegetationIgnitionWeatherMultiplier();
|
||||||
|
if (IsWetWeatherActive())
|
||||||
|
{
|
||||||
|
Multiplier *= 0.5f;
|
||||||
|
}
|
||||||
|
|
||||||
|
return FMath::Max(0.0f, Multiplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
float AAgrarianCampfire::GetActiveBurningFuelScore() const
|
||||||
|
{
|
||||||
|
float GrassFuelScore = 0.0f;
|
||||||
|
float ForestFuelScore = 0.0f;
|
||||||
|
const float VegetationFuelScore = GetVegetationFuelScoreNearFire(GrassFuelScore, ForestFuelScore);
|
||||||
|
const float StructureFuelScore = GetStructureFuelScoreNearFire();
|
||||||
|
|
||||||
|
return FMath::Max(0.0f, VegetationFuelScore + StructureFuelScore);
|
||||||
|
}
|
||||||
|
|||||||
@@ -150,6 +150,30 @@ public:
|
|||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Structure", meta = (ClampMin = "0"))
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Structure", meta = (ClampMin = "0"))
|
||||||
float StructureIgnitionRiskPerSecond = 0.08f;
|
float StructureIgnitionRiskPerSecond = 0.08f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = "Agrarian|Fire|Spread", meta = (ClampMin = "0", ClampMax = "100"))
|
||||||
|
float GrassFireIntensity = 0.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = "Agrarian|Fire|Spread", meta = (ClampMin = "0", ClampMax = "100"))
|
||||||
|
float ForestFireIntensity = 0.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = "Agrarian|Fire|Spread", meta = (ClampMin = "0", ClampMax = "100"))
|
||||||
|
float StructureFireIntensity = 0.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Replicated, Category = "Agrarian|Fire|Spread", meta = (ClampMin = "0"))
|
||||||
|
float ActiveFireSpreadRadius = 0.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Spread", meta = (ClampMin = "0"))
|
||||||
|
float BaseFireSpreadRadius = 250.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Spread", meta = (ClampMin = "0"))
|
||||||
|
float MaxFireSpreadRadius = 3500.0f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Spread", meta = (ClampMin = "0"))
|
||||||
|
float FireSpreadIntensityPerSecond = 0.18f;
|
||||||
|
|
||||||
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Spread", meta = (ClampMin = "0"))
|
||||||
|
float FireSuppressionPressure = 0.0f;
|
||||||
|
|
||||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Weather", meta = (ClampMin = "1"))
|
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Agrarian|Fire|Weather", meta = (ClampMin = "1"))
|
||||||
float RainFuelDrainMultiplier = 1.5f;
|
float RainFuelDrainMultiplier = 1.5f;
|
||||||
|
|
||||||
@@ -219,4 +243,7 @@ protected:
|
|||||||
float GetVegetationIgnitionWeatherMultiplier() const;
|
float GetVegetationIgnitionWeatherMultiplier() const;
|
||||||
void UpdateStructureIgnitionRisk(float DeltaSeconds);
|
void UpdateStructureIgnitionRisk(float DeltaSeconds);
|
||||||
float GetStructureFuelScoreNearFire() const;
|
float GetStructureFuelScoreNearFire() const;
|
||||||
|
void UpdateServerAuthoritativeFireSpread(float DeltaSeconds);
|
||||||
|
float GetFireSpreadWeatherMultiplier() const;
|
||||||
|
float GetActiveBurningFuelScore() const;
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user