Anders GIDENSTAM: wildfire (read $FG_ROOT/Models/Effects/Wildfire/README)
This commit is contained in:
parent
aa96adf5a0
commit
1fe5ace8b5
3 changed files with 1224 additions and 0 deletions
254
Nasal/mp_broadcast.nas
Normal file
254
Nasal/mp_broadcast.nas
Normal file
|
@ -0,0 +1,254 @@
|
|||
###############################################################################
|
||||
## $Id$
|
||||
##
|
||||
## A message based information broadcast for the multiplayer network.
|
||||
##
|
||||
## Copyright (C) 2008 Anders Gidenstam (anders(at)gidenstam.org)
|
||||
## This file is licensed under the GPL license version 2 or later.
|
||||
##
|
||||
###############################################################################
|
||||
|
||||
###############################################################################
|
||||
# Broadcast primitive using a MP enabled string property.
|
||||
#
|
||||
# BroadcastChannel.new(mpp_path, process)
|
||||
# Create a new broadcast primitive. Any MP user with the same
|
||||
# primitive will receive all messages sent to the channel from the point
|
||||
# she/he joined (barring severe MP packet loss).
|
||||
# NOTE: Message delivery is not guaranteed.
|
||||
# mpp_path - MP property path : string
|
||||
# process - handler called when receiving a message : func (n, msg)
|
||||
# n is the base node of the senders property tree
|
||||
# (i.e. /ai/models/multiplay[x])
|
||||
# send_to_self - if 1 locally sent messages are : int {0,1}
|
||||
# delivered just like remote messages.
|
||||
# If 0 locally sent messages are not delivered
|
||||
# to the local receiver.
|
||||
# accept_predicate - function to select which : func (p)
|
||||
# multiplayers to listen to.
|
||||
# p is the multiplayer entry node.
|
||||
# The default is to accept any multiplayer.
|
||||
# on_disconnect - function to be called when an : func (p)
|
||||
# accepted MP user leaves.
|
||||
# enable_send - Set to 0 to disable sending.
|
||||
#
|
||||
# BroadcastChannel.send(msg)
|
||||
# Sends the message msg to the channel.
|
||||
# msg - text string with Binary data encoded data : string
|
||||
#
|
||||
# BroadcastChannel.die()
|
||||
# Destroy this BroadcastChannel instance.
|
||||
#
|
||||
var BroadcastChannel = {};
|
||||
BroadcastChannel.new = func (mpp_path, process,
|
||||
send_to_self = 0,
|
||||
accept_predicate = nil,
|
||||
on_disconnect = nil,
|
||||
enable_send=1) {
|
||||
obj = { parents : [BroadcastChannel],
|
||||
mpp_path : mpp_path,
|
||||
send_node : enable_send ? props.globals.getNode(mpp_path, 1)
|
||||
: nil,
|
||||
process_msg : process,
|
||||
send_to_self : send_to_self,
|
||||
accept_predicate : (accept_predicate != nil) ? accept_predicate :
|
||||
func (p) { return 1; },
|
||||
on_disconnect : (on_disconnect != nil) ? on_disconnect :
|
||||
func (p) { return; },
|
||||
# Internal state.
|
||||
send_buf : [],
|
||||
peers : {},
|
||||
loopid : 0,
|
||||
PERIOD : 1.3,
|
||||
last_time : 0.0, # For join handling.
|
||||
last_send : 0.0, # For the send queue
|
||||
SEND_TIME : 0.5 };
|
||||
if (enable_send and (obj.send_node == nil)) {
|
||||
printlog("warn",
|
||||
"BroadcastChannel invalid send node.");
|
||||
return nil;
|
||||
}
|
||||
settimer(func { obj._loop_(obj.loopid); }, 0, 1);
|
||||
|
||||
return obj;
|
||||
}
|
||||
BroadcastChannel.send = func (msg) {
|
||||
if (me.send_node == nil) return;
|
||||
|
||||
var t = getprop("/sim/time/elapsed-sec");
|
||||
if (((t - me.last_send) > me.SEND_TIME) and (size(me.send_buf) == 0)) {
|
||||
me.send_node.setValue(msg);
|
||||
me.last_send = t;
|
||||
if (me.send_to_self) me.process_msg(props.globals, msg);
|
||||
} else {
|
||||
append(me.send_buf, msg);
|
||||
}
|
||||
}
|
||||
BroadcastChannel.die = func {
|
||||
me.loopid += 1;
|
||||
# print("BroadcastChannel[" ~ me.mpp_path ~ "] ... destroyed.");
|
||||
}
|
||||
|
||||
############################################################
|
||||
# Internals.
|
||||
BroadcastChannel.update = func {
|
||||
var t = getprop("/sim/time/elapsed-sec");
|
||||
var process_msg = me.process_msg;
|
||||
|
||||
# Handled join/leave. This is done more seldom.
|
||||
if ((t - me.last_time) > me.PERIOD) {
|
||||
var mpplayers =
|
||||
props.globals.getNode("/ai/models").getChildren("multiplayer");
|
||||
foreach (var pilot; mpplayers) {
|
||||
if ((pilot.getChild("valid") != nil) and
|
||||
pilot.getChild("valid").getValue()) {
|
||||
if (me.accept_predicate(pilot) and
|
||||
me.peers[pilot.getIndex()] == nil) {
|
||||
me.peers[pilot.getIndex()] =
|
||||
MessageChannel.new(pilot.getNode(me.mpp_path),
|
||||
func(msg) {
|
||||
# print("BroadcastChannel: processing " ~ msg);
|
||||
process_msg(pilot, msg);
|
||||
});
|
||||
}
|
||||
} else {
|
||||
delete(me.peers, pilot.getIndex());
|
||||
me.on_disconnect(pilot);
|
||||
}
|
||||
}
|
||||
me.last_time = t;
|
||||
}
|
||||
# Process new messages.
|
||||
foreach (var w; keys(me.peers)) {
|
||||
if (me.peers[w] != nil) me.peers[w].update();
|
||||
}
|
||||
# Check send buffer.
|
||||
if (me.send_node == nil) return;
|
||||
|
||||
if ((t - me.last_send) > me.SEND_TIME) {
|
||||
if (size(me.send_buf) > 0) {
|
||||
me.send_node.setValue(me.send_buf[0]);
|
||||
me.send_buf = subvec(me.send_buf, 1);
|
||||
me.last_send = t;
|
||||
if (me.send_to_self) me.process_msg(props.globals, msg);
|
||||
} else {
|
||||
# Nothing new to send. Reset the send property to save bandwidth.
|
||||
me.send_node.setValue("");
|
||||
}
|
||||
}
|
||||
}
|
||||
BroadcastChannel._loop_ = func(id) {
|
||||
id == me.loopid or return;
|
||||
me.update();
|
||||
settimer(func { me._loop_(id); }, 0, 1);
|
||||
}
|
||||
######################################################################
|
||||
|
||||
###############################################################################
|
||||
# Some routines for encoding/decoding values into/from a string.
|
||||
# NOTE: MP is picky about what it sends in a string propery.
|
||||
# Encode 7 bits as a printable 8 bit character.
|
||||
var Binary = {};
|
||||
Binary.TWOTO31 = 2147483648;
|
||||
Binary.TWOTO32 = 4294967296;
|
||||
Binary.sizeOf = {};
|
||||
############################################################
|
||||
Binary.sizeOf["int"] = 5;
|
||||
Binary.encodeInt = func (int) {
|
||||
var bf = bits.buf(5);
|
||||
if (int < 0) int += Binary.TWOTO32;
|
||||
var r = int;
|
||||
for (var i = 0; i < 5; i += 1) {
|
||||
var c = math.mod(r, 128);
|
||||
bf[4-i] = c + `A`;
|
||||
r = (r - c)/128;
|
||||
}
|
||||
return bf;
|
||||
}
|
||||
############################################################
|
||||
Binary.decodeInt = func (str) {
|
||||
var v = 0;
|
||||
var b = 1;
|
||||
for (var i = 0; i < 5; i += 1) {
|
||||
v += (str[4-i] - `A`) * b;
|
||||
b *= 128;
|
||||
}
|
||||
if (v / Binary.TWOTO31 >= 1) v -= Binary.TWOTO32;
|
||||
return int(v);
|
||||
}
|
||||
############################################################
|
||||
# NOTE: This encodes a 7 bit byte.
|
||||
Binary.sizeOf["byte"] = 1;
|
||||
Binary.encodeByte = func (int) {
|
||||
var bf = bits.buf(1);
|
||||
if (int < 0) int += 128;
|
||||
bf[0] = math.mod(int, 128) + `A`;
|
||||
return bf;
|
||||
}
|
||||
############################################################
|
||||
Binary.decodeByte = func (str) {
|
||||
var v = str[0] - `A`;
|
||||
if (v / 64 >= 1) v -= 128;
|
||||
return int(v);
|
||||
}
|
||||
############################################################
|
||||
# NOTE: This can neither handle huge values nor really tiny.
|
||||
Binary.sizeOf["double"] = 2*Binary.sizeOf["int"];
|
||||
Binary.encodeDouble = func (d) {
|
||||
return Binary.encodeInt(int(d)) ~
|
||||
Binary.encodeInt((d - int(d)) * Binary.TWOTO31);
|
||||
}
|
||||
############################################################
|
||||
Binary.decodeDouble = func (str) {
|
||||
return Binary.decodeInt(substr(str, 0)) +
|
||||
Binary.decodeInt(substr(str, 5)) / Binary.TWOTO31;
|
||||
}
|
||||
############################################################
|
||||
# Encodes a geo.Coord object.
|
||||
Binary.sizeOf["Coord"] = 3*Binary.sizeOf["double"];
|
||||
Binary.encodeCoord = func (coord) {
|
||||
return Binary.encodeDouble(coord.lat()) ~
|
||||
Binary.encodeDouble(coord.lon()) ~
|
||||
Binary.encodeDouble(coord.alt());
|
||||
}
|
||||
############################################################
|
||||
# Decodes an encoded geo.Coord object.
|
||||
Binary.decodeCoord = func (str) {
|
||||
var coord = geo.aircraft_position();
|
||||
coord.set_latlon(Binary.decodeDouble(substr(str, 0)),
|
||||
Binary.decodeDouble(substr(str, 10)),
|
||||
Binary.decodeDouble(substr(str, 20)));
|
||||
return coord;
|
||||
}
|
||||
######################################################################
|
||||
|
||||
###############################################################################
|
||||
# Detects incomming messages encoded in a string property.
|
||||
# n - MP source : property node
|
||||
# process - action : func (v)
|
||||
# NOTE: This is a low level component.
|
||||
# The same object is seldom used for both sending and receiving.
|
||||
var MessageChannel = {};
|
||||
MessageChannel.new = func (n = nil, process = nil) {
|
||||
obj = { parents : [MessageChannel],
|
||||
node : n,
|
||||
process_msg : process,
|
||||
old : "" };
|
||||
return obj;
|
||||
}
|
||||
MessageChannel.update = func {
|
||||
if (me.node == nil) return;
|
||||
|
||||
var msg = me.node.getValue();
|
||||
if (!streq(typeof(msg), "scalar")) return;
|
||||
|
||||
if ((me.process_msg != nil) and
|
||||
!streq(msg, "") and
|
||||
!streq(msg, me.old)) {
|
||||
me.process_msg(msg);
|
||||
me.old = msg;
|
||||
}
|
||||
}
|
||||
MessageChannel.send = func (msg) {
|
||||
me.node.setValue(msg);
|
||||
}
|
962
Nasal/wildfire.nas
Normal file
962
Nasal/wildfire.nas
Normal file
|
@ -0,0 +1,962 @@
|
|||
###############################################################################
|
||||
## $Id$
|
||||
##
|
||||
## A cellular automaton forest fire model with the ability to
|
||||
## spread over the multiplayer network.
|
||||
##
|
||||
## Copyright (C) 2007 - 2009 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 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/";
|
||||
|
||||
###############################################################################
|
||||
## 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=-1) {
|
||||
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 report_score_pp = "environment/wildfire/report-score";
|
||||
# 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 = 80;
|
||||
|
||||
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;
|
||||
}
|
||||
settimer(score_report_loop, CAFire.GENERATION_DURATION);
|
||||
}
|
||||
|
||||
###############################################################################
|
||||
# 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)) return;
|
||||
var type = Binary.decodeByte(substr(msg, 5));
|
||||
if (type == 1) {
|
||||
var pos = Binary.decodeCoord(substr(msg, 6));
|
||||
ignite(pos, 0);
|
||||
}
|
||||
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 one fire cell.
|
||||
|
||||
var FireCell = {
|
||||
############################################################
|
||||
new : func (x, y) {
|
||||
# print("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];
|
||||
}
|
||||
}
|
||||
m.model = CellModel.new(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;
|
||||
me.model.set_type("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)) {
|
||||
me.model.set_type("soot");
|
||||
} else {
|
||||
# Use a model representing contamination here.
|
||||
me.model.set_type(type);
|
||||
}
|
||||
return result;
|
||||
},
|
||||
############################################################
|
||||
update : func () {
|
||||
# print("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];
|
||||
me.model.set_type(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 (CAFire.MODEL[type] == "") return;
|
||||
|
||||
# Always put "cheap" models for now.
|
||||
if (CAFire.models_enabled or (type != "fire")) {
|
||||
me.model =
|
||||
geo.put_model(CAFire.MODEL[type], me.lat, me.lon, me.alt);
|
||||
# print("Created 3d model " ~ type ~ " " ~ CAFire.MODEL[type]);
|
||||
}
|
||||
},
|
||||
############################################################
|
||||
remove : func() {
|
||||
if (me.model != nil) me.model.remove();
|
||||
me.model = nil;
|
||||
}
|
||||
############################################################
|
||||
};
|
||||
|
||||
###############################################################################
|
||||
# Singleton that maintains the fire 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.loopid = 0;
|
||||
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.MODEL = { # Model paths
|
||||
"fire" : "Models/Effects/Wildfire/wildfire.xml",
|
||||
"soot" : "Models/Effects/Wildfire/soot.xml",
|
||||
"foam" : "Models/Effects/Wildfire/foam.xml",
|
||||
"water" : "",
|
||||
"protected" : "",
|
||||
"none" : "",
|
||||
};
|
||||
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.
|
||||
setlistener(models_enabled_pp, func (n) {
|
||||
me.set_models_enabled(n.getValue());
|
||||
}, 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 grid.
|
||||
foreach (var x; keys(me.grid)) {
|
||||
foreach (var y; keys(me.grid[x])) {
|
||||
if (me.grid[x][y].model != nil) me.grid[x][y].model.remove();
|
||||
me.grid[x][y] = nil;
|
||||
}
|
||||
}
|
||||
# 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;
|
||||
me.loopid += 1;
|
||||
me._loop_(me.loopid);
|
||||
}
|
||||
############################################################
|
||||
# Start a fire in the cell at pos.
|
||||
CAFire.ignite = func (lat, lon) {
|
||||
# print("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) {
|
||||
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));
|
||||
# print("Dumping water at " ~ lat ~", " ~ lon ~
|
||||
# ". Center (" ~ x ~ "," ~ y ~ ") radius " ~ r ~".");
|
||||
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) {
|
||||
# print(" (" ~ (x + dx) ~ ", " ~ (y + dy) ~ ")");
|
||||
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 water 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) {
|
||||
return me.resolve_water_drop(lat, lon, radius, volume);
|
||||
}
|
||||
############################################################
|
||||
# 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) {
|
||||
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));
|
||||
# print("Dumping foam at " ~ lat ~", " ~ lon ~
|
||||
# ". Center (" ~ x ~ "," ~ y ~ ") radius " ~ r ~".");
|
||||
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) {
|
||||
# print(" (" ~ (x + dx) ~ ", " ~ (y + dy) ~ ")");
|
||||
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]);
|
||||
|
||||
# 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=-1) {
|
||||
me.load_count += 1;
|
||||
var logbase = "/tmp/wildfire-load-log[" ~ me.load_count ~ "]";
|
||||
if (!fgcommand("loadxml",
|
||||
props.Node.new({ filename : filename,
|
||||
targetnode : logbase }))) {
|
||||
printlog("warn", "Wildfire ... failed loading '" ~ filename ~ "'");
|
||||
return;
|
||||
}
|
||||
|
||||
# Fast forward the automaton from the first logged event to the current time.
|
||||
me.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 (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();
|
||||
}
|
||||
me.set_models_enabled(getprop(models_enabled_pp));
|
||||
}
|
||||
############################################################
|
||||
CAFire.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 there select animations, though.
|
||||
}
|
||||
######################################################################
|
||||
# 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(id) {
|
||||
id == me.loopid or return;
|
||||
me.update();
|
||||
settimer(func { me._loop_(id); },
|
||||
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(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(fire_LOD_pp, 10, "INT");
|
||||
props.globals.initNode(smoke_LOD_pp, 10, "INT");
|
||||
|
||||
SimTime.init();
|
||||
CAFire.init();
|
||||
broadcast =
|
||||
mp_broadcast.BroadcastChannel.new(msg_channel_mpp, parse_msg);
|
||||
|
||||
setlistener("/sim/signals/exit", func {
|
||||
if (getprop(report_score_pp))
|
||||
print_score();
|
||||
if (getprop(save_on_exit_pp))
|
||||
CAFire.save_event_log(SAVEDIR ~ "fire_log.xml");
|
||||
});
|
||||
|
||||
if (getprop(restore_on_startup_pp)) {
|
||||
settimer(func {
|
||||
# Delay loading the log until the terrain is there. Note: hack.
|
||||
CAFire.load_event_log(SAVEDIR ~ "fire_log.xml", 1);
|
||||
}, 3);
|
||||
}
|
||||
# Start the score reporting.
|
||||
settimer(score_report_loop, CAFire.GENERATION_DURATION);
|
||||
|
||||
# Detect aircraft crash.
|
||||
setlistener("sim/crashed", func(n) {
|
||||
if (getprop(crash_fire_pp) and n.getBoolValue())
|
||||
wildfire.ignite(geo.aircraft_position());
|
||||
});
|
||||
|
||||
printlog("info", "Wildfire ... initialized.");
|
||||
});
|
||||
###############################################################################
|
||||
|
||||
###############################################################################
|
||||
## 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");
|
||||
var w = titlebar.addChild("button");
|
||||
w.set("pref-width", 16);
|
||||
w.set("pref-height", 16);
|
||||
w.set("legend", "");
|
||||
w.set("default", 0);
|
||||
w.set("key", "esc");
|
||||
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],
|
||||
["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");
|
||||
|
||||
# Load button.
|
||||
var load = me.dialog.addChild("button");
|
||||
load.node.setValues({"legend" : "Load Wildfire log",
|
||||
"halign" : "center"});
|
||||
load.setBinding("nasal",
|
||||
"wildfire.dialog.select_and_load()");
|
||||
|
||||
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(100, 100);
|
||||
me.create();
|
||||
}
|
||||
},
|
||||
#################################################################
|
||||
select_and_load : func {
|
||||
var selector = gui.FileSelector.new
|
||||
(func (n) { CAFire.load_event_log(n.getValue()); },
|
||||
"Load Wildfire log", # dialog title
|
||||
"Load", # button text
|
||||
["*.xml"], # pattern for files
|
||||
SAVEDIR, # start dir
|
||||
"fire_log.xml"); # default file name
|
||||
selector.open();
|
||||
}
|
||||
}
|
||||
###############################################################################
|
|
@ -311,6 +311,14 @@
|
|||
<dialog-name>rainsnow</dialog-name>
|
||||
</binding>
|
||||
</item>
|
||||
|
||||
<item>
|
||||
<label>Wildfire Settings</label>
|
||||
<binding>
|
||||
<command>nasal</command>
|
||||
<script>wildfire.dialog.show()</script>
|
||||
</binding>
|
||||
</item>
|
||||
</menu>
|
||||
|
||||
<menu>
|
||||
|
|
Loading…
Add table
Reference in a new issue