1
0
Fork 0
fgdata/Nasal/wildfire.nas
Henning Stahlke 148328d793 Nasal loadpriority; remove nasal-dir-initialized listeners;
add more dependencies to loadpriority.xml and remove _setlistener stuff
2020-06-03 21:57:14 +01:00

1190 lines
41 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;
###############################################################################
## 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 {
logprint(LOG_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 {
wildfire_score_CAFireModels_loop_timer.restart(0.033);
}
############################################################
# Stop the CA model grid.
# Note that it will catch up lost time when started again.
CAFireModels.stop = func {
wildfire_score_CAFireModels_loop_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);
}
}
}
# 30hz
wildfire_score_CAFireModels_loop_timer = maketimer(0.033, CAFireModels, CAFireModels.update);
###############################################################################
###############################################################################
# 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.
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();
wildfire_score_CAFire_loop_timer.start();
}
############################################################
# Stop the CA. Note that it will catch up lost time when started again.
CAFire.stop = func {
CAFireModels.stop();
broadcast.stop();
wildfire_score_CAFire_loop_timer.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 }))) {
logprint(LOG_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) {
logprint(LOG_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();
wildfire_score_CAFire_loop_timer.restart(me.GENERATION_DURATION * (me.generation + 1/me.PASSES) -
SimTime.current_time());
}
###############################################################################
wildfire_score_report_loop_timer = maketimer(CAFire.GENERATION_DURATION, score_report_loop);
wildfire_score_report_loop_timer.simulatedTime = 1;
wildfire_score_CAFire_loop_timer = maketimer(CAFire.GENERATION_DURATION, CAFire, CAFire._loop_ );
wildfire_score_CAFire_loop_timer.simulatedTime = 1;
###############################################################################
# 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();
}
};
###############################################################################
###############################################################################
# Main initialization.
var Binary = mp_broadcast.Binary;
# Create configuration properties if they do not 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.
wildfire_score_report_loop_timer.restart(CAFire.GENERATION_DURATION);
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()) {
logprint(LOG_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) {
logprint(LOG_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);
}
});
logprint(LOG_INFO, "Wildfire ... initialized.");