1
0
Fork 0
fgdata/Nasal/wildfire.nas
Richard Harrison 40ff13b65b Reposition slowdown settimer fixes.
Definitely problems in aar.nas, seaport.nas; as these were hanging off a fdm-initialized.

The others have timers that are started from a listener and as such are more suited to use maketimer rather than settimer

The modules that use the loopid technique are probably fine, but these are also more suited (and easier to understand) using a maketimer
2018-09-14 22:50:47 +02:00

1195 lines
42 KiB
Text

###############################################################################
##
## A cellular automaton forest fire model with the ability to
## spread over the multiplayer network.
##
## Copyright (C) 2007 - 2012 Anders Gidenstam (anders(at)gidenstam.org)
## This file is licensed under the GPL license version 2 or later.
##
###############################################################################
# The cellular automata model used here is loosely based on
# A. Hernandez Encinas, L. Hernandez Encinas, S. Hoya White,
# A. Martin del Rey, G. Rodriguez Sanchez,
# "Simulation of forest fire fronts using cellular automata",
# Advances in Engineering Software 38 (2007), pp. 372-378, Elsevier.
# Set this to print for debug.
var trace = func {}
# Where to save fire event logs.
var SAVEDIR = getprop("/sim/fg-home") ~ "/Wildfire/";
# Maximum number of ignite events a single user can send per second.
var MAX_IGNITE_RATE = 0.25;
var wildfireScoreReportTimer = nil;
###############################################################################
## External API
# Start a fire.
# pos - fire location : geo.Coord
# source - broadcast event? : bool
var ignite = func (pos, source=1) {
if (!getprop(CA_enabled_pp)) return;
if (getprop(MP_share_pp) and source) broadcast.send(ignition_msg(pos));
CAFire.ignite(pos.lat(), pos.lon());
}
# Resolve a water drop impact.
# pos - drop location : geo.Coord
# radius - drop radius m : double
# volume - drop volume m3 : double
var resolve_water_drop = func (pos, radius, volume, source=1) {
if (!getprop(CA_enabled_pp)) return;
if (getprop(MP_share_pp) and source) {
broadcast.send(water_drop_msg(pos, radius, volume));
}
var res = CAFire.resolve_water_drop(pos.lat(), pos.lon(), radius, volume);
if (source) {
score.extinguished += res.extinguished;
score.protected += res.protected;
score.waste += res.waste;
}
}
# Resolve a retardant drop impact.
# pos - drop location : geo.Coord
# radius - drop radius : double
# volume - drop volume : double
var resolve_retardant_drop = func (pos, radius, volume, source=1) {
if (!getprop(CA_enabled_pp)) return;
if (getprop(MP_share_pp) and source) {
broadcast.send(retardant_drop_msg(pos, radius, volume));
}
var res = CAFire.resolve_retardant_drop(pos.lat(), pos.lon(),
radius, volume);
if (source) {
score.extinguished += res.extinguished;
score.protected += res.protected;
score.waste += res.waste;
}
}
# Resolve a foam drop impact.
# pos - drop location : geo.Coord
# radius - drop radius : double
# volume - drop volume : double
var resolve_foam_drop = func (pos, radius, volume, source=1) {
if (!getprop(CA_enabled_pp)) return;
if (getprop(MP_share_pp) and source) {
broadcast.send(foam_drop_msg(pos, radius, volume));
}
var res = CAFire.resolve_foam_drop(pos.lat(), pos.lon(),
radius, volume);
if (source) {
score.extinguished += res.extinguished;
score.protected += res.protected;
score.waste += res.waste;
}
}
# Load an event log.
# skip_ahead_until - skip from last event to this time : double (epoch)
# fast forward from skip_ahead_until
# to current time.
# x < last event - fast forward all the way to current time (use 0).
# NOTE: Can be VERY time consuming.
# -1 - skip to current time.
var load_event_log = func (filename, skip_ahead_until) {
CAFire.load_event_log(filename, skip_ahead_until);
}
# Save an event log.
#
var save_event_log = func (filename) {
CAFire.save_event_log(filename);
}
# Print current score summary.
var print_score = func {
print("Wildfire drop summary: #extinguished cells: " ~ score.extinguished ~
" #protected cells: " ~ score.protected ~
" #wasted: " ~ score.waste);
print("Wildfire fire summary: #created cells: " ~ CAFire.cells_created ~
" #cells still burning: " ~ CAFire.cells_burning);
}
###############################################################################
# Internals.
###############################################################################
var msg_channel_mpp = "environment/wildfire/data";
var broadcast = nil;
var seq = 0;
# Configuration properties
var CA_enabled_pp = "environment/wildfire/enabled";
var MP_share_pp = "environment/wildfire/share-events";
var save_on_exit_pp = "environment/wildfire/save-on-exit";
var restore_on_startup_pp = "environment/wildfire/restore-on-startup";
var crash_fire_pp = "environment/wildfire/fire-on-crash";
var impact_fire_pp = "environment/wildfire/fire-on-impact";
var report_score_pp = "environment/wildfire/report-score";
var event_file_pp = "environment/wildfire/events-file";
var time_hack_pp = "environment/wildfire/time-hack-gmt";
# Format: "yyyy:mm:dd:hh:mm:ss"
# Internal properties to control the models
var models_enabled_pp = "environment/wildfire/models/enabled";
var fire_LOD_pp = "environment/wildfire/models/fire-lod";
var smoke_LOD_pp = "environment/wildfire/models/smoke-lod";
var LOD_High = 20;
var LOD_Low = 50;
var mp_last_limited_event = {}; # source : time
var score = { extinguished : 0, protected : 0, waste : 0 };
var old_score = { extinguished : 0, protected : 0, waste : 0 };
###############################################################################
# Utility functions.
var score_report_loop = func {
if ((score.extinguished > old_score.extinguished) or
(score.protected > old_score.protected)) {
if (getprop(report_score_pp)) {
setprop("/sim/messages/copilot",
"Extinguished " ~ (score.extinguished - old_score.extinguished) ~
" fire cells.");
}
old_score.extinguished = score.extinguished;
old_score.protected = score.protected;
old_score.waste = score.waste;
} else {
if (getprop(report_score_pp) and (score.waste > old_score.waste))
setprop("/sim/messages/copilot",
"Miss!");
old_score.extinguished = score.extinguished;
old_score.protected = score.protected;
old_score.waste = score.waste;
}
}
###############################################################################
# MP messages
var ignition_msg = func (pos) {
seq += 1;
return Binary.encodeInt(seq) ~ Binary.encodeByte(1) ~
Binary.encodeCoord(pos);
}
var water_drop_msg = func (pos, radius, volume) {
seq += 1;
return Binary.encodeInt(seq) ~ Binary.encodeByte(2) ~
Binary.encodeCoord(pos) ~ Binary.encodeDouble(radius);
}
var retardant_drop_msg = func (pos, radius, volume) {
seq += 1;
return Binary.encodeInt(seq) ~ Binary.encodeByte(3) ~
Binary.encodeCoord(pos) ~ Binary.encodeDouble(radius);
}
var foam_drop_msg = func (pos, radius, volume) {
seq += 1;
return Binary.encodeInt(seq) ~ Binary.encodeByte(4) ~
Binary.encodeCoord(pos) ~ Binary.encodeDouble(radius);
}
var parse_msg = func (source, msg) {
if (!getprop(MP_share_pp) or !getprop(CA_enabled_pp)) return;
var cur_time = systime();
var type = Binary.decodeByte(substr(msg, 5));
if (type == 1) {
var i = source.getIndex();
if (!contains(mp_last_limited_event, i) or
(cur_time - mp_last_limited_event[i]) > 1/MAX_IGNITE_RATE) {
var pos = Binary.decodeCoord(substr(msg, 6));
ignite(pos, 0);
} else {
printlog("alert", "wildfire.nas: Ignored ignite event flood from " ~
source.getNode("callsign").getValue());
}
mp_last_limited_event[i] = cur_time;
}
if (type == 2) {
var pos = Binary.decodeCoord(substr(msg, 6));
var radius = Binary.decodeDouble(substr(msg, 36));
resolve_water_drop(pos, radius, 0, 0);
}
if (type == 3) {
var pos = Binary.decodeCoord(substr(msg, 6));
var radius = Binary.decodeDouble(substr(msg, 36));
resolve_retardant_drop(pos, radius, 0, 0);
}
if (type == 4) {
var pos = Binary.decodeCoord(substr(msg, 6));
var radius = Binary.decodeDouble(substr(msg, 36));
resolve_foam_drop(pos, radius, 0, 0);
}
}
###############################################################################
# Simulation time management.
# NOTE: Time warp is ignored for the time being.
var SimTime = {
############################################################
init : func {
# Sim time is me.real_time_base + warp + sim-elapsed-sec
me.real_time_base = systime();
me.elapsed_time = props.globals.getNode("/sim/time/elapsed-sec");
},
current_time : func {
return me.real_time_base + me.elapsed_time.getValue();
}
};
###############################################################################
# Class that maintains the state of one fire cell.
var FireCell = {
############################################################
new : func (x, y) {
trace("Creating FireCell[" ~ x ~ "," ~ y ~ "]");
var m = { parents: [FireCell] };
m.lat = y * CAFire.CELL_SIZE/60.0 + 0.5 * CAFire.CELL_SIZE / 60.0;
m.lon = x * CAFire.CELL_SIZE/60.0 + 0.5 * CAFire.CELL_SIZE / 60.0;
m.x = x;
m.y = y;
m.state = [0.0, 0.0]; # burned area / total area.
m.burning = [0, 0]; # {0,1} Not intensity but could become, maybe
m.last = 0; # Last update generation.
# Fetch ground type.
var geo_info = geodinfo(m.lat, m.lon);
if ((geo_info == nil) or (geo_info[1] == nil) or
(geo_info[1].names == nil)) return nil;
m.alt = geo_info[0];
m.burn_rate = 0.0;
foreach (var mat; geo_info[1].names) {
trace("Material: " ~ mat);
if (CAFire.BURN_RATE[mat] != nil) {
if (CAFire.BURN_RATE[mat] > m.burn_rate)
m.burn_rate = CAFire.BURN_RATE[mat];
}
}
CAFireModels.add(x, y, m.alt);
append(CAFire.active, m);
CAFire.cells_created += 1;
return m;
},
############################################################
ignite : func {
if ((me.state[CAFire.old] < 1) and (me.burn_rate > 0)) {
trace("FireCell[" ~ me.x ~ "," ~me.y ~ "] Ignited!");
me.burning[CAFire.next] = 1;
me.burning[CAFire.old] = 1;
CAFireModels.set_type(me.x, me.y, "fire");
# Prevent update() on this cell in this generation.
me.last = CAFire.generation;
} else {
trace("FireCell[" ~ me.lat ~ "," ~me.lon ~ "] Failed to ignite!");
}
},
############################################################
extinguish : func (type="soot") {
trace("FireCell[" ~ me.x ~ "," ~ me.y ~ "] extinguished.");
var result = 0;
if (me.burning[CAFire.old]) result = 1;
if (me.burn_rate == 0) result = -1; # A waste to protect this cell.
if (me.state[CAFire.next] > 1) me.state[CAFire.next] = 1;
me.burning[CAFire.next] = 0;
me.burn_rate = 0; # This cell is nonflammable now.
# Prevent update() on this cell in this generation.
me.last = CAFire.generation;
if ((me.state[CAFire.old] > 0.0) and (me.burning[CAFire.old] > 0)) {
CAFireModels.set_type(me.x, me.y, "soot");
} else {
# Use a model representing contamination here.
CAFireModels.set_type(me.x, me.y, type);
}
return result;
},
############################################################
update : func () {
trace("FireCell[" ~ me.x ~ "," ~me.y ~ "] " ~ me.state[CAFire.old]);
if ((me.state[CAFire.old] == 1) and (me.burning[CAFire.old] == 0))
return 0;
if ((me.burn_rate == 0) and (me.burning[CAFire.old] == 0))
return 0;
if (me.last >= CAFire.generation) return 1; # Some event has happened here.
me.last = CAFire.generation;
me.state[CAFire.next] = me.state[CAFire.old] +
(me.burning[CAFire.old] * me.burn_rate +
me.get_neighbour_burn((me.state[CAFire.old] > CAFire.IGNITE_THRESHOLD))
) * CAFire.GENERATION_DURATION;
if ((me.burning[CAFire.old] == 0) and
(0 < me.state[CAFire.next]) and (me.state[CAFire.old] < 1)) {
me.ignite();
return 1;
}
if (me.state[CAFire.next] >= 1) {
me.extinguish("soot");
return 0;
}
if (me.burn_rate == 0) {
# Does this make sense?
me.extinguish("protected");
return 0;
}
me.burning[CAFire.next] = me.burning[CAFire.old];
CAFireModels.set_type(me.x, me.y, me.burning[CAFire.old] ? "fire" : "soot");
return 1;
},
############################################################
# Get neightbour burn values.
get_neighbour_burn : func (create) {
var burn = 0.0;
foreach (var d; CAFire.NEIGHBOURS[0]) {
var c = CAFire.get_cell(me.x + d[0], me.y + d[1]);
if (c != nil) {
burn += c.burning[CAFire.old] * c.burn_rate *
(5*me.alt / c.alt) *
c.state[CAFire.old] * CAFire.GENERATION_DURATION;
} else {
if (create) {
# Create the neighbour.
CAFire.set_cell(me.x + d[0], me.y + d[1],
FireCell.new(me.x + d[0],
me.y + d[1]));
}
}
}
foreach (var d; CAFire.NEIGHBOURS[1]) {
var c = CAFire.get_cell(me.x + d[0], me.y + d[1]);
if (c != nil) {
burn += 0.785 * c.burning[CAFire.old] * c.burn_rate *
(5*me.alt / c.alt) *
c.state[CAFire.old] * CAFire.GENERATION_DURATION;
} else {
if (create) {
# Create the neighbour.
CAFire.set_cell(me.x + d[0], me.y + d[1],
FireCell.new(me.x + d[0],
me.y + d[1]));
}
}
}
return burn;
},
############################################################
};
###############################################################################
# Class that maintains the 3d model(s) for one fire cell.
var CellModel = {
############################################################
new : func (x, y, alt) {
var m = { parents: [CellModel] };
m.type = "none";
m.model = nil;
m.lat = y * CAFire.CELL_SIZE/60.0 + 0.5 * CAFire.CELL_SIZE / 60.0;
m.lon = x * CAFire.CELL_SIZE/60.0 + 0.5 * CAFire.CELL_SIZE / 60.0;
m.x = x;
m.y = y;
m.alt = alt + 0.1;
return m;
},
############################################################
set_type : func(type) {
if (me.model != nil) {
if (me.type == type) return;
me.model.remove();
me.model = nil;
}
me.type = type;
if (CAFireModels.MODEL[type] == "") return;
# Always put "cheap" models for now.
if (CAFireModels.models_enabled or (type != "fire")) {
me.model =
geo.put_model(CAFireModels.MODEL[type], me.lat, me.lon, me.alt);
trace("Created 3d model " ~ type ~ " " ~ CAFireModels.MODEL[type]);
}
},
############################################################
remove : func() {
if (me.model != nil) me.model.remove();
me.model = nil;
}
############################################################
};
###############################################################################
# Singleton that maintains the CA models.
var CAFireModels = {};
# Constants
CAFireModels.MODEL = { # Model paths
"fire" : "Models/Effects/Wildfire/wildfire.xml",
"soot" : "Models/Effects/Wildfire/soot.xml",
"foam" : "Models/Effects/Wildfire/foam.xml",
"water" : "",
"retardant" : "Models/Effects/Wildfire/retardant.xml",
"protected" : "",
"none" : "",
};
# State
CAFireModels.grid = {}; # Sparse cell model grid storage.
CAFireModels.pending = []; # List of pending model changes.
CAFireModels.models_enabled = 1;
######################################################################
# Public operations
############################################################
CAFireModels.init = func {
# Initialization.
setlistener(models_enabled_pp, func (n) {
me.set_models_enabled(n.getValue());
}, 1);
me.reset(1);
}
############################################################
# Reset the model grid to the empty state.
CAFireModels.reset = func (enabled) {
# Clear the model grid.
foreach (var x; keys(me.grid)) {
foreach (var y; keys(me.grid[x])) {
if (me.grid[x][y] != nil) me.grid[x][y].remove();
}
}
# Reset state.
me.grid = {};
me.pending = [];
if (enabled) {
me.start();
}
}
############################################################
# Start the CA model grid.
CAFireModels.start = func {
me.timer.restart(0.033); # 30hz should be fast enough.
}
############################################################
# Stop the CA model grid.
# Note that it will catch up lost time when started again.
CAFireModels.stop = func {
me.timer.stop();
}
############################################################
# Add a new cell model.
CAFireModels.add = func(x, y, alt) {
append(me.pending, { x: x, y: y, alt: alt });
}
############################################################
# Update a cell model.
CAFireModels.set_type = func(x, y, type) {
append(me.pending, { x: x, y: y, type: type });
}
############################################################
CAFireModels.set_models_enabled = func(on=1) {
me.models_enabled = on;
# We should do a pass over all cells here to add/remove models.
# For now I don't so only active cells will actually remove the
# models. All models will be hidden by their select animations, though.
}
######################################################################
# Private operations
############################################################
CAFireModels.update = func {
var work = size(me.pending)/10;
while (size(me.pending) > 0 and work > 0) {
var c = me.pending[0];
me.pending = subvec(me.pending, 1);
work -= 1;
if (contains(c, "alt")) {
if (me.grid[c.x] == nil) {
me.grid[c.x] = {};
}
me.grid[c.x][c.y] = CellModel.new(c.x, c.y, c.alt);
}
if (contains(c, "type")) {
me.grid[c.x][c.y].set_type(c.type);
}
}
}
###############################################################################
###############################################################################
# Singleton that maintains the fire cell CA grid.
var CAFire = {};
# State
CAFire.CELL_SIZE = 0.03; # "nm" (or rather minutes)
CAFire.GENERATION_DURATION = 4.0; # seconds
CAFire.PASSES = 8.0; # Passes per full update.
CAFire.IGNITE_THRESHOLD = 0.3; # Minimum cell state for igniting neighbours.
CAFire.grid = {}; # Sparse cell grid storage.
CAFire.generation = 0; # CA generation. Defined from the epoch.
CAFire.enabled = 0;
CAFire.active = []; # List of active cells. These will be updated.
CAFire.old = 0; # selects new/old cell state.
CAFire.next = 1; # selects new/old cell state.
CAFire.cells_created = 0;
CAFire.cells_burning = 0;
CAFire.pass = 0; # Update pass within the current full update.
CAFire.pass_work = 0; # Cells to update in each pass.
CAFire.remaining_work = []; # Work remaining in this full update.
CAFire.event_log = []; # List of all events that has occured so far.
CAFire.load_count = 0;
CAFire.BURN_RATE = { # Burn rate DB. grid widths per second
# Grass
"Grass" : 0.0010,
"grass_rwy" : 0.0010,
"ShrubGrassCover" : 0.0010,
"ScrubCover" : 0.0010,
"BareTundraCover" : 0.0010,
"MixedTundraCover" : 0.0010,
"HerbTundraCover" : 0.0010,
"MixedCropPastureCover" : 0.0010,
"DryCropPastureCover" : 0.0010,
"CropGrassCover" : 0.0010,
"CropWoodCover" : 0.0010,
# Forest
"DeciduousBroadCover" : 0.0005,
"EvergreenBroadCover" : 0.0005,
"MixedForestCover" : 0.0005,
"EvergreenNeedleCover" : 0.0005,
"WoodedTundraCover" : 0.0005,
"DeciduousNeedleCover" : 0.0005,
# City
"BuiltUpCover" : 0.0005,
# ?
"Landmass" : 0.0005
};
CAFire.NEIGHBOURS = # Neighbour index offsets. First row and column
# and then diagonal.
[[[-1, 0], [0, 1], [1, 0], [0, -1]],
[[-1, 1], [1, 1], [1, -1], [-1, -1]]];
######################################################################
# Public operations
############################################################
CAFire.init = func {
# Initialization.
CAFireModels.timer = maketimer(0.03, me, me.loop);
CAFireModels.timer.simulatedTime = 1;
me.reset(1, SimTime.current_time());
}
############################################################
# Reset the CA to the empty state and set its current time to sim_time.
CAFire.reset = func (enabled, sim_time) {
# Clear the model grid.
CAFireModels.reset(enabled);
# Reset state.
me.grid = {};
me.generation = int(sim_time/CAFire.GENERATION_DURATION);
me.active = [];
me.old = 0;
me.next = 1;
me.cells_created = 0;
me.cells_burning = 0;
me.pass = 0;
me.pass_work = 0;
me.remaining_work = [];
me.event_log = [];
me.enabled = enabled;
if (me.enabled) {
me.start();
} else {
me.stop();
}
}
############################################################
# Start the CA.
CAFire.start = func {
CAFireModels.start();
broadcast.start();
}
############################################################
# Stop the CA. Note that it will catch up lost time when started again.
CAFire.stop = func {
CAFireModels.stop();
broadcast.stop();
}
############################################################
# Start a fire in the cell at pos.
CAFire.ignite = func (lat, lon) {
trace("CAFire.ignite: Fire at " ~ lat ~", " ~ lon ~ ".");
var x = int(lon*60/me.CELL_SIZE);
var y = int(lat*60/me.CELL_SIZE);
var cell = me.get_cell(x, y);
if (cell == nil) {
cell = FireCell.new(x, y);
me.set_cell(x, y,
cell);
}
if (cell != nil) cell.ignite();
append(me.event_log, [SimTime.current_time(), "ignite", lat, lon]);
}
############################################################
# Resolve a water drop.
# For now: Assume that water makes the affected cell nonflammable forever
# and extinguishes it if burning.
# radius - meter : double
# Note: volume is unused ATM.
CAFire.resolve_water_drop = func (lat, lon, radius, volume=0) {
trace("CAFire.resolve_water_drop: Dumping water at " ~ lat ~", " ~ lon ~
" radius " ~ radius ~".");
var x = int(lon*60/me.CELL_SIZE);
var y = int(lat*60/me.CELL_SIZE);
var r = int(2*radius/(me.CELL_SIZE*1852.0));
var result = { extinguished : 0, protected : 0, waste : 0 };
for (var dx = -r; dx <= r; dx += 1) {
for (var dy = -r; dy <= r; dy += 1) {
var cell = me.get_cell(x + dx, y + dy);
if (cell == nil) {
cell = FireCell.new(x + dx, y + dy);
me.set_cell(x + dx, y + dy,
cell);
}
if (cell != nil) {
var res = cell.extinguish("water");
if (res > 0) {
result.extinguished += 1;
} else {
if (res == 0) result.protected += 1;
else result.waste += 1;
}
} else {
result.waste += 1;
}
}
}
append(me.event_log,
[SimTime.current_time(), "water_drop", lat, lon, radius]);
return result;
}
############################################################
# Resolve a fire retardant drop.
# For now: Assume that the retardant makes the affected cell nonflammable
# forever and extinguishes it if burning.
# Note: volume is unused ATM.
CAFire.resolve_retardant_drop = func (lat, lon, radius, volume=0) {
trace("CAFire.resolve_retardant_drop: Dumping retardant at " ~
lat ~", " ~ lon ~ " radius " ~ radius ~".");
var x = int(lon*60/me.CELL_SIZE);
var y = int(lat*60/me.CELL_SIZE);
var r = int(2*radius/(me.CELL_SIZE*1852.0));
var result = { extinguished : 0, protected : 0, waste : 0 };
for (var dx = -r; dx <= r; dx += 1) {
for (var dy = -r; dy <= r; dy += 1) {
var cell = me.get_cell(x + dx, y + dy);
if (cell == nil) {
cell = FireCell.new(x + dx, y + dy);
me.set_cell(x + dx, y + dy,
cell);
}
if (cell != nil) {
var res = cell.extinguish("retardant");
if (res > 0) {
result.extinguished += 1;
} else {
if (res == 0) result.protected += 1;
else result.waste += 1;
}
} else {
result.waste += 1;
}
}
}
append(me.event_log,
[SimTime.current_time(), "retardant_drop", lat, lon, radius]);
return result;
}
############################################################
# Resolve a foam drop.
# For now: Assume that water makes the affected cell nonflammable forever
# and extinguishes it if burning.
# radius - meter : double
# Note: volume is unused ATM.
CAFire.resolve_foam_drop = func (lat, lon, radius, volume=0) {
trace("CAFire.resolve_foam_drop: Dumping foam at " ~ lat ~", " ~ lon ~
" radius " ~ radius ~".");
var x = int(lon*60/me.CELL_SIZE);
var y = int(lat*60/me.CELL_SIZE);
var r = int(2*radius/(me.CELL_SIZE*1852.0));
var result = { extinguished : 0, protected : 0, waste : 0 };
for (var dx = -r; dx <= r; dx += 1) {
for (var dy = -r; dy <= r; dy += 1) {
var cell = me.get_cell(x + dx, y + dy);
if (cell == nil) {
cell = FireCell.new(x + dx, y + dy);
me.set_cell(x + dx, y + dy,
cell);
}
if (cell != nil) {
var res = cell.extinguish("foam");
if (res > 0) {
result.extinguished += 1;
} else {
if (res == 0) result.protected += 1;
else result.waste += 1;
}
} else {
result.waste += 1;
}
}
}
append(me.event_log,
[SimTime.current_time(), "foam_drop", lat, lon, radius]);
return result;
}
############################################################
# Save the current event log.
# This is modelled on Melchior FRANZ's ac_state.nas.
CAFire.save_event_log = func (filename) {
var args = props.Node.new({ filename : filename });
var data = args.getNode("data", 1);
gui.popupTip("Wildfire: Saving state to " ~ filename);
var i = 0;
foreach (var e; me.event_log) {
var event = data.getNode("event[" ~ i ~ "]", 1);
event.getNode("time-sec", 1).setDoubleValue(e[0]);
event.getNode("type", 1).setValue(e[1]);
event.getNode("latitude", 1).setDoubleValue(e[2]);
event.getNode("longitude", 1).setDoubleValue(e[3]);
# Event type specific data.
if (e[1] == "water_drop")
event.getNode("radius", 1).setDoubleValue(e[4]);
if (e[1] == "foam_drop")
event.getNode("radius", 1).setDoubleValue(e[4]);
if (e[1] == "retardant_drop")
event.getNode("radius", 1).setDoubleValue(e[4]);
# debug.dump(e);
i += 1;
}
# Add save event to aid skip ahead restore.
var event = data.getNode("event[" ~ i ~ "]", 1);
event.getNode("time-sec", 1).setDoubleValue(SimTime.current_time());
event.getNode("type", 1).setValue("save");
fgcommand("savexml", args);
}
############################################################
# Load an event log.
# skip_ahead_until - skip from last event to this time : double (epoch)
# fast forward from skip_ahead_until
# to current time.
# x < last event - fast forward all the way to current time (use 0).
# -1 - skip to current time.
CAFire.load_event_log = func (filename, skip_ahead_until) {
me.load_count += 1;
var logbase = "/tmp/wildfire-load-log[" ~ me.load_count ~ "]";
if (!fgcommand("loadxml",
props.Node.new({ filename : filename,
targetnode : logbase }))) {
printlog("alert", "Wildfire ... failed loading '" ~ filename ~ "'");
return;
}
# Fast forward the automaton from the first logged event to the current time.
CAFireModels.set_models_enabled(0);
var first = 1;
var events = props.globals.getNode(logbase).getChildren("event");
foreach (var event; events) {
if (first) {
first = 0;
me.reset(1, event.getNode("time-sec").getValue());
}
# print("[" ~
# event.getNode("time-sec").getValue() ~ "," ~
# event.getNode("type").getValue() ~ "]");
var e = [event.getNode("time-sec").getValue(),
event.getNode("type").getValue()];
# Fast forward state.
while (me.generation * me.GENERATION_DURATION < e[0]) {
# print("between event ff " ~ me.generation);
me.update();
}
# Apply event. Note: The logged time is wrong ATM.
if (event.getNode("type").getValue() == "ignite") {
me.ignite(event.getNode("latitude").getValue(),
event.getNode("longitude").getValue());
me.event_log[size(me.event_log) - 1][0] = e[0];
}
if (event.getNode("type").getValue() == "water_drop") {
me.resolve_water_drop(event.getNode("latitude").getValue(),
event.getNode("longitude").getValue(),
event.getNode("radius").getValue());
me.event_log[size(me.event_log) - 1][0] = e[0];
}
if (event.getNode("type").getValue() == "foam_drop") {
me.resolve_foam_drop(event.getNode("latitude").getValue(),
event.getNode("longitude").getValue(),
event.getNode("radius").getValue());
me.event_log[size(me.event_log) - 1][0] = e[0];
}
if (event.getNode("type").getValue() == "retardant_drop") {
me.resolve_retardant_drop(event.getNode("latitude").getValue(),
event.getNode("longitude").getValue(),
event.getNode("radius").getValue());
me.event_log[size(me.event_log) - 1][0] = e[0];
}
}
if (first) {
me.reset(1, SimTime.current_time());
return;
}
var now = SimTime.current_time();
if (skip_ahead_until == -1) {
me.generation = int(now/me.GENERATION_DURATION);
} else {
if (me.generation < int(skip_ahead_until/me.GENERATION_DURATION)) {
me.generation = int(skip_ahead_until/me.GENERATION_DURATION);
}
# Catch up with current time. NOTE: This can be very time consuming!
while (me.generation * me.GENERATION_DURATION < now)
me.update();
}
CAFireModels.set_models_enabled(getprop(models_enabled_pp));
}
######################################################################
# Internal operations
CAFire.get_cell = func (x, y) {
if (me.grid[x] == nil) me.grid[x] = {};
return me.grid[x][y];
}
############################################################
CAFire.set_cell = func (x, y, cell) {
if (me.grid[x] == nil) {
me.grid[x] = {};
}
me.grid[x][y] = cell;
}
############################################################
CAFire.update = func {
if (!me.enabled) return; # The CA is disabled.
if (me.pass == me.PASSES) {
# Setup a new main iteration.
me.generation += 1;
me.pass = 0;
me.remaining_work = me.active;
me.active = [];
me.pass_work = size(me.remaining_work)/ me.PASSES + 1;
if (me.old == 1) {
me.old = 0;
me.next = 1;
} else {
me.old = 1;
me.next = 0;
}
if (me.cells_burning > 0) {
printlog("info",
"Wildfire: generation " ~ me.generation ~ " updating " ~
size(me.remaining_work) ~" / " ~ me.cells_created ~
" created cells. " ~ me.cells_burning ~ " burning cells.");
}
# Set LOD.
if (LOD_Low <= me.cells_burning) {
props.globals.getNode(fire_LOD_pp).setIntValue(1);
props.globals.getNode(smoke_LOD_pp).setIntValue(1);
}
if ((LOD_High <= me.cells_burning) and (me.cells_burning < LOD_Low)) {
props.globals.getNode(fire_LOD_pp).setIntValue(5);
props.globals.getNode(smoke_LOD_pp).setIntValue(5);
}
if (me.cells_burning < LOD_High) {
props.globals.getNode(fire_LOD_pp).setIntValue(10);
props.globals.getNode(smoke_LOD_pp).setIntValue(10);
}
me.cells_burning = 0;
}
me.pass += 1;
var work = me.pass_work;
var c = pop(me.remaining_work);
while (c != nil) {
if (c.update() != 0) {
append(me.active, c);
me.cells_burning += c.burning[me.next];
}
work -= 1;
if (work <= 0) return;
c = pop(me.remaining_work);
}
}
############################################################
CAFire.loop = func {
me.update();
CAFireModels.timer.restart(me.GENERATION_DURATION
* (me.generation + 1/me.PASSES)
- SimTime.current_time()
);
}
###############################################################################
###############################################################################
# Main initialization.
var Binary = nil;
_setlistener("/sim/signals/nasal-dir-initialized", func {
Binary = mp_broadcast.Binary;
# Create configuration properties if they don't exist already.
props.globals.initNode(CA_enabled_pp, 1, "BOOL");
setlistener(CA_enabled_pp, func (n) {
if (getprop("/sim/signals/reinit")) return; # Ignore resets.
CAFire.reset(n.getValue(), SimTime.current_time());
});
props.globals.initNode(MP_share_pp, 1, "BOOL");
props.globals.initNode(crash_fire_pp, 1, "BOOL");
props.globals.initNode(impact_fire_pp, 1, "BOOL");
props.globals.initNode(save_on_exit_pp, 0, "BOOL");
props.globals.initNode(restore_on_startup_pp, 0, "BOOL");
props.globals.initNode(models_enabled_pp, 1, "BOOL");
props.globals.initNode(report_score_pp, 1, "BOOL");
props.globals.initNode(event_file_pp, "", "STRING");
props.globals.initNode(time_hack_pp, "", "STRING");
props.globals.initNode(fire_LOD_pp, 10, "INT");
props.globals.initNode(smoke_LOD_pp, 10, "INT");
SimTime.init();
broadcast =
mp_broadcast.BroadcastChannel.new(msg_channel_mpp, parse_msg);
CAFire.init();
# Start the score reporting.
if (wildfireScoreReportTimer == nil){
wildfireScoreReportTimer = maketimer(CAFire.GENERATION_DURATION, score_report_loop);
wildfireScoreReportTimer.simulatedTime = 1;
wildfireScoreReportTimer.start();
}
setlistener("/sim/signals/exit", func {
if (getprop(report_score_pp) and (CAFire.cells_created > 0))
print_score();
if (getprop(save_on_exit_pp))
CAFire.save_event_log(SAVEDIR ~ "fire_log.xml");
});
# Determine the skip-ahead-to time, if any.
var time_hack = time_string_to_epoch(getprop(time_hack_pp));
if (time_hack > SimTime.current_time()) {
printlog("alert",
"wildfire.nas: Ignored time hack " ~
(SimTime.current_time() - time_hack) ~
" seconds into the future.");
# Skip ahead to current time instead.
time_hack = -1;
} elsif (time_hack > 0) {
printlog("alert",
"wildfire.nas: Time hack " ~
(SimTime.current_time() - time_hack) ~
" seconds ago.");
} else {
# Skip ahead to current time instead.
time_hack = -1;
}
if (getprop(event_file_pp) != "") {
settimer(func {
# Delay loading the log until the terrain is there. Note: hack.
CAFire.load_event_log(getprop(event_file_pp), time_hack);
}, 3);
} elsif (getprop(restore_on_startup_pp)) {
settimer(func {
# Delay loading the log until the terrain is there. Note: hack.
# Restore skips ahead to current time.
CAFire.load_event_log(SAVEDIR ~ "fire_log.xml", -1);
}, 3);
}
# Detect aircraft crash.
setlistener("sim/crashed", func(n) {
if (getprop(crash_fire_pp) and n.getBoolValue())
wildfire.ignite(geo.aircraft_position());
});
# Detect impact.
var impact_node = props.globals.getNode("sim/ai/aircraft/impact/bomb", 1);
setlistener("sim/ai/aircraft/impact/bomb", func(n) {
if (getprop(impact_fire_pp) and n.getBoolValue()){
var node = props.globals.getNode(n.getValue(), 1);
var impactpos = geo.Coord.new();
impactpos.set_latlon
(node.getNode("impact/latitude-deg").getValue(),
node.getNode("impact/longitude-deg").getValue());
wildfire.ignite(impactpos);
}
});
printlog("info", "Wildfire ... initialized.");
});
###############################################################################
###############################################################################
# Utility functions
# Convert a time string in the format yyyy[:mm[:dd[:hh[:mm[:ss]]]]]
# to seconds since 1970:01:01:00:00:00.
#
# Note: This is an over simplified approximation.
var time_string_to_epoch = func (time) {
var res = [];
if (string.scanf(time, "%d:%d:%d:%d:%d:%d", var res1 = []) != 0) {
res = res1;
} elsif (string.scanf(time, "%d:%d:%d:%d:%d", var res2 = []) != 0) {
res = res2 ~ [0];
} elsif (string.scanf(time, "%d:%d:%d:%d", var res3 = []) != 0) {
res = res3 ~ [0, 0];
} elsif (string.scanf(time, "%d:%d:%d", var res4 = []) != 0) {
res = res4 ~ [0, 0, 0];
} elsif (string.scanf(time, "%d:%d", var res5 = []) != 0) {
res = res5 ~ [0, 0, 0, 0];
} elsif (string.scanf(time, "%d", var res6 = []) != 0) {
res = res6 ~ [0, 0, 0, 0, 0];
} else {
return -1;
}
return
(res[0] - 1970) * 3.15569e7 +
(res[1] - 1) * 2.63e+6 +
(res[2] - 1) * 86400 +
res[3] * 3600 +
res[4] * 60 +
res[5];
}
###############################################################################
## WildFire configuration dialog.
## Partly based on Till Bush's multiplayer dialog
var CONFIG_DLG = 0;
var dialog = {
#################################################################
init : func (x = nil, y = nil) {
me.x = x;
me.y = y;
me.bg = [0, 0, 0, 0.3]; # background color
me.fg = [[1.0, 1.0, 1.0, 1.0]];
#
# "private"
me.title = "Wildfire";
me.basenode = props.globals.getNode("/environment/wildfire");
me.dialog = nil;
me.namenode = props.Node.new({"dialog-name" : me.title });
me.listeners = [];
},
#################################################################
create : func {
if (me.dialog != nil)
me.close();
me.dialog = gui.Widget.new();
me.dialog.set("name", me.title);
if (me.x != nil)
me.dialog.set("x", me.x);
if (me.y != nil)
me.dialog.set("y", me.y);
me.dialog.set("layout", "vbox");
me.dialog.set("default-padding", 0);
var titlebar = me.dialog.addChild("group");
titlebar.set("layout", "hbox");
titlebar.addChild("empty").set("stretch", 1);
titlebar.addChild("text").set("label", "Wildfire Settings");
titlebar.addChild("empty").set("stretch", 1);
var w = titlebar.addChild("button");
w.set("pref-width", 16);
w.set("pref-height", 16);
w.set("legend", "");
w.set("default", 0);
w.setBinding("nasal", "wildfire.dialog.destroy(); ");
w.setBinding("dialog-close");
me.dialog.addChild("hrule");
var content = me.dialog.addChild("group");
content.set("layout", "vbox");
content.set("halign", "center");
content.set("default-padding", 5);
foreach (var b; [["Enabled", CA_enabled_pp],
["Share over MP", MP_share_pp],
["Show 3d models", models_enabled_pp],
["Crash starts fire", crash_fire_pp],
["Impact starts fire", impact_fire_pp],
["Report score", report_score_pp],
["Save on exit", save_on_exit_pp]]) {
var w = content.addChild("checkbox");
w.node.setValues({"label" : b[0],
"halign" : "left",
"property" : b[1]});
w.setBinding("nasal",
"setprop(\"" ~ b[1] ~ "\"," ~
"!getprop(\"" ~ b[1] ~ "\"))");
}
me.dialog.addChild("hrule");
# Buttons
var buttons = me.dialog.addChild("group");
buttons.node.setValues({"layout" : "hbox"});
# Load button.
var load = buttons.addChild("button");
load.node.setValues({"legend" : "Load Wildfire log",
"halign" : "center"});
load.setBinding("nasal",
"wildfire.dialog.select_and_load()");
# Close button
var close = buttons.addChild("button");
close.node.setValues({"legend" : "Close",
"default" : "true",
"key" : "Esc"});
close.setBinding("nasal", "wildfire.dialog.destroy();");
close.setBinding("dialog-close");
fgcommand("dialog-new", me.dialog.prop());
fgcommand("dialog-show", me.namenode);
},
#################################################################
close : func {
fgcommand("dialog-close", me.namenode);
},
#################################################################
destroy : func {
CONFIG_DLG = 0;
me.close();
foreach(var l; me.listeners)
removelistener(l);
delete(gui.dialog, "\"" ~ me.title ~ "\"");
},
#################################################################
show : func {
if (!CONFIG_DLG) {
CONFIG_DLG = 1;
me.init();
me.create();
}
},
#################################################################
select_and_load : func {
var selector = gui.FileSelector.new
(func (n) { load_event_log(n.getValue(), -1); },
"Load Wildfire log", # dialog title
"Load", # button text
["*.xml"], # pattern for files
SAVEDIR, # start dir
"fire_log.xml"); # default file name
selector.open();
}
}
###############################################################################