1 ###############################################################################
3 ## A cellular automaton forest fire model with the ability to
4 ## spread over the multiplayer network.
6 ## Copyright (C) 2007 - 2012 Anders Gidenstam (anders(at)gidenstam.org)
7 ## This file is licensed under the GPL license version 2 or later.
9 ###############################################################################
11 # The cellular automata model used here is loosely based on
12 # A. Hernandez Encinas, L. Hernandez Encinas, S. Hoya White,
13 # A. Martin del Rey, G. Rodriguez Sanchez,
14 # "Simulation of forest fire fronts using cellular automata",
15 # Advances in Engineering Software 38 (2007), pp. 372-378, Elsevier.
17 # Set this to print for debug.
20 # Where to save fire event logs.
21 var SAVEDIR = getprop("/sim/fg-home") ~ "/Wildfire/";
23 # Maximum number of ignite events a single user can send per second.
24 var MAX_IGNITE_RATE = 0.25;
26 ###############################################################################
30 # pos - fire location : geo.Coord
31 # source - broadcast event? : bool
32 var ignite = func (pos, source=1) {
33 if (!getprop(CA_enabled_pp)) return;
34 if (getprop(MP_share_pp) and source) broadcast.send(ignition_msg(pos));
35 CAFire.ignite(pos.lat(), pos.lon());
38 # Resolve a water drop impact.
39 # pos - drop location : geo.Coord
40 # radius - drop radius m : double
41 # volume - drop volume m3 : double
42 var resolve_water_drop = func (pos, radius, volume, source=1) {
43 if (!getprop(CA_enabled_pp)) return;
44 if (getprop(MP_share_pp) and source) {
45 broadcast.send(water_drop_msg(pos, radius, volume));
47 var res = CAFire.resolve_water_drop(pos.lat(), pos.lon(), radius, volume);
49 score.extinguished += res.extinguished;
50 score.protected += res.protected;
51 score.waste += res.waste;
55 # Resolve a retardant drop impact.
56 # pos - drop location : geo.Coord
57 # radius - drop radius : double
58 # volume - drop volume : double
59 var resolve_retardant_drop = func (pos, radius, volume, source=1) {
60 if (!getprop(CA_enabled_pp)) return;
61 if (getprop(MP_share_pp) and source) {
62 broadcast.send(retardant_drop_msg(pos, radius, volume));
64 var res = CAFire.resolve_retardant_drop(pos.lat(), pos.lon(),
67 score.extinguished += res.extinguished;
68 score.protected += res.protected;
69 score.waste += res.waste;
73 # Resolve a foam drop impact.
74 # pos - drop location : geo.Coord
75 # radius - drop radius : double
76 # volume - drop volume : double
77 var resolve_foam_drop = func (pos, radius, volume, source=1) {
78 if (!getprop(CA_enabled_pp)) return;
79 if (getprop(MP_share_pp) and source) {
80 broadcast.send(foam_drop_msg(pos, radius, volume));
82 var res = CAFire.resolve_foam_drop(pos.lat(), pos.lon(),
85 score.extinguished += res.extinguished;
86 score.protected += res.protected;
87 score.waste += res.waste;
92 # skip_ahead_until - skip from last event to this time : double (epoch)
93 # fast forward from skip_ahead_until
95 # x < last event - fast forward all the way to current time (use 0).
96 # NOTE: Can be VERY time consuming.
97 # -1 - skip to current time.
98 var load_event_log = func (filename, skip_ahead_until) {
99 CAFire.load_event_log(filename, skip_ahead_until);
104 var save_event_log = func (filename) {
105 CAFire.save_event_log(filename);
108 # Print current score summary.
109 var print_score = func {
110 print("Wildfire drop summary: #extinguished cells: " ~ score.extinguished ~
111 " #protected cells: " ~ score.protected ~
112 " #wasted: " ~ score.waste);
113 print("Wildfire fire summary: #created cells: " ~ CAFire.cells_created ~
114 " #cells still burning: " ~ CAFire.cells_burning);
117 ###############################################################################
119 ###############################################################################
121 var msg_channel_mpp = "environment/wildfire/data";
124 # Configuration properties
125 var CA_enabled_pp = "environment/wildfire/enabled";
126 var MP_share_pp = "environment/wildfire/share-events";
127 var save_on_exit_pp = "environment/wildfire/save-on-exit";
128 var restore_on_startup_pp = "environment/wildfire/restore-on-startup";
129 var crash_fire_pp = "environment/wildfire/fire-on-crash";
130 var impact_fire_pp = "environment/wildfire/fire-on-impact";
131 var report_score_pp = "environment/wildfire/report-score";
132 var event_file_pp = "environment/wildfire/events-file";
133 var time_hack_pp = "environment/wildfire/time-hack-gmt";
134 # Format: "yyyy:mm:dd:hh:mm:ss"
135 # Internal properties to control the models
136 var models_enabled_pp = "environment/wildfire/models/enabled";
137 var fire_LOD_pp = "environment/wildfire/models/fire-lod";
138 var smoke_LOD_pp = "environment/wildfire/models/smoke-lod";
141 var mp_last_limited_event = {}; # source : time
143 var score = { extinguished : 0, protected : 0, waste : 0 };
144 var old_score = { extinguished : 0, protected : 0, waste : 0 };
146 ###############################################################################
148 var score_report_loop = func {
149 if ((score.extinguished > old_score.extinguished) or
150 (score.protected > old_score.protected)) {
151 if (getprop(report_score_pp)) {
152 setprop("/sim/messages/copilot",
153 "Extinguished " ~ (score.extinguished - old_score.extinguished) ~
156 old_score.extinguished = score.extinguished;
157 old_score.protected = score.protected;
158 old_score.waste = score.waste;
160 if (getprop(report_score_pp) and (score.waste > old_score.waste))
161 setprop("/sim/messages/copilot",
163 old_score.extinguished = score.extinguished;
164 old_score.protected = score.protected;
165 old_score.waste = score.waste;
167 settimer(score_report_loop, CAFire.GENERATION_DURATION);
170 ###############################################################################
173 var ignition_msg = func (pos) {
175 return Binary.encodeInt(seq) ~ Binary.encodeByte(1) ~
176 Binary.encodeCoord(pos);
179 var water_drop_msg = func (pos, radius, volume) {
181 return Binary.encodeInt(seq) ~ Binary.encodeByte(2) ~
182 Binary.encodeCoord(pos) ~ Binary.encodeDouble(radius);
185 var retardant_drop_msg = func (pos, radius, volume) {
187 return Binary.encodeInt(seq) ~ Binary.encodeByte(3) ~
188 Binary.encodeCoord(pos) ~ Binary.encodeDouble(radius);
191 var foam_drop_msg = func (pos, radius, volume) {
193 return Binary.encodeInt(seq) ~ Binary.encodeByte(4) ~
194 Binary.encodeCoord(pos) ~ Binary.encodeDouble(radius);
197 var parse_msg = func (source, msg) {
198 if (!getprop(MP_share_pp) or !getprop(CA_enabled_pp)) return;
199 var cur_time = systime();
200 var type = Binary.decodeByte(substr(msg, 5));
202 var i = source.getIndex();
203 if (!contains(mp_last_limited_event, i) or
204 (cur_time - mp_last_limited_event[i]) > 1/MAX_IGNITE_RATE) {
205 var pos = Binary.decodeCoord(substr(msg, 6));
208 printlog("alert", "wildfire.nas: Ignored ignite event flood from " ~
209 source.getNode("callsign").getValue());
211 mp_last_limited_event[i] = cur_time;
214 var pos = Binary.decodeCoord(substr(msg, 6));
215 var radius = Binary.decodeDouble(substr(msg, 36));
216 resolve_water_drop(pos, radius, 0, 0);
219 var pos = Binary.decodeCoord(substr(msg, 6));
220 var radius = Binary.decodeDouble(substr(msg, 36));
221 resolve_retardant_drop(pos, radius, 0, 0);
224 var pos = Binary.decodeCoord(substr(msg, 6));
225 var radius = Binary.decodeDouble(substr(msg, 36));
226 resolve_foam_drop(pos, radius, 0, 0);
230 ###############################################################################
231 # Simulation time management.
232 # NOTE: Time warp is ignored for the time being.
235 ############################################################
237 # Sim time is me.real_time_base + warp + sim-elapsed-sec
238 me.real_time_base = systime();
239 me.elapsed_time = props.globals.getNode("/sim/time/elapsed-sec");
241 current_time : func {
242 return me.real_time_base + me.elapsed_time.getValue();
247 ###############################################################################
248 # Class that maintains the state of one fire cell.
250 ############################################################
252 trace("Creating FireCell[" ~ x ~ "," ~ y ~ "]");
253 var m = { parents: [FireCell] };
254 m.lat = y * CAFire.CELL_SIZE/60.0 + 0.5 * CAFire.CELL_SIZE / 60.0;
255 m.lon = x * CAFire.CELL_SIZE/60.0 + 0.5 * CAFire.CELL_SIZE / 60.0;
258 m.state = [0.0, 0.0]; # burned area / total area.
259 m.burning = [0, 0]; # {0,1} Not intensity but could become, maybe
260 m.last = 0; # Last update generation.
263 var geo_info = geodinfo(m.lat, m.lon);
264 if ((geo_info == nil) or (geo_info[1] == nil) or
265 (geo_info[1].names == nil)) return nil;
268 foreach (var mat; geo_info[1].names) {
269 trace("Material: " ~ mat);
270 if (CAFire.BURN_RATE[mat] != nil) {
271 if (CAFire.BURN_RATE[mat] > m.burn_rate)
272 m.burn_rate = CAFire.BURN_RATE[mat];
275 CAFireModels.add(x, y, m.alt);
276 append(CAFire.active, m);
277 CAFire.cells_created += 1;
280 ############################################################
282 if ((me.state[CAFire.old] < 1) and (me.burn_rate > 0)) {
283 trace("FireCell[" ~ me.x ~ "," ~me.y ~ "] Ignited!");
284 me.burning[CAFire.next] = 1;
285 me.burning[CAFire.old] = 1;
286 CAFireModels.set_type(me.x, me.y, "fire");
287 # Prevent update() on this cell in this generation.
288 me.last = CAFire.generation;
290 trace("FireCell[" ~ me.lat ~ "," ~me.lon ~ "] Failed to ignite!");
293 ############################################################
294 extinguish : func (type="soot") {
295 trace("FireCell[" ~ me.x ~ "," ~ me.y ~ "] extinguished.");
297 if (me.burning[CAFire.old]) result = 1;
298 if (me.burn_rate == 0) result = -1; # A waste to protect this cell.
300 if (me.state[CAFire.next] > 1) me.state[CAFire.next] = 1;
301 me.burning[CAFire.next] = 0;
302 me.burn_rate = 0; # This cell is nonflammable now.
303 # Prevent update() on this cell in this generation.
304 me.last = CAFire.generation;
305 if ((me.state[CAFire.old] > 0.0) and (me.burning[CAFire.old] > 0)) {
306 CAFireModels.set_type(me.x, me.y, "soot");
308 # Use a model representing contamination here.
309 CAFireModels.set_type(me.x, me.y, type);
313 ############################################################
315 trace("FireCell[" ~ me.x ~ "," ~me.y ~ "] " ~ me.state[CAFire.old]);
316 if ((me.state[CAFire.old] == 1) and (me.burning[CAFire.old] == 0))
318 if ((me.burn_rate == 0) and (me.burning[CAFire.old] == 0))
320 if (me.last >= CAFire.generation) return 1; # Some event has happened here.
321 me.last = CAFire.generation;
323 me.state[CAFire.next] = me.state[CAFire.old] +
324 (me.burning[CAFire.old] * me.burn_rate +
325 me.get_neighbour_burn((me.state[CAFire.old] > CAFire.IGNITE_THRESHOLD))
326 ) * CAFire.GENERATION_DURATION;
328 if ((me.burning[CAFire.old] == 0) and
329 (0 < me.state[CAFire.next]) and (me.state[CAFire.old] < 1)) {
333 if (me.state[CAFire.next] >= 1) {
334 me.extinguish("soot");
337 if (me.burn_rate == 0) {
338 # Does this make sense?
339 me.extinguish("protected");
342 me.burning[CAFire.next] = me.burning[CAFire.old];
343 CAFireModels.set_type(me.x, me.y, me.burning[CAFire.old] ? "fire" : "soot");
346 ############################################################
347 # Get neightbour burn values.
348 get_neighbour_burn : func (create) {
350 foreach (var d; CAFire.NEIGHBOURS[0]) {
351 var c = CAFire.get_cell(me.x + d[0], me.y + d[1]);
353 burn += c.burning[CAFire.old] * c.burn_rate *
355 c.state[CAFire.old] * CAFire.GENERATION_DURATION;
358 # Create the neighbour.
359 CAFire.set_cell(me.x + d[0], me.y + d[1],
360 FireCell.new(me.x + d[0],
365 foreach (var d; CAFire.NEIGHBOURS[1]) {
366 var c = CAFire.get_cell(me.x + d[0], me.y + d[1]);
368 burn += 0.785 * c.burning[CAFire.old] * c.burn_rate *
370 c.state[CAFire.old] * CAFire.GENERATION_DURATION;
373 # Create the neighbour.
374 CAFire.set_cell(me.x + d[0], me.y + d[1],
375 FireCell.new(me.x + d[0],
382 ############################################################
385 ###############################################################################
386 # Class that maintains the 3d model(s) for one fire cell.
388 ############################################################
389 new : func (x, y, alt) {
390 var m = { parents: [CellModel] };
393 m.lat = y * CAFire.CELL_SIZE/60.0 + 0.5 * CAFire.CELL_SIZE / 60.0;
394 m.lon = x * CAFire.CELL_SIZE/60.0 + 0.5 * CAFire.CELL_SIZE / 60.0;
400 ############################################################
401 set_type : func(type) {
402 if (me.model != nil) {
403 if (me.type == type) return;
408 if (CAFireModels.MODEL[type] == "") return;
410 # Always put "cheap" models for now.
411 if (CAFireModels.models_enabled or (type != "fire")) {
413 geo.put_model(CAFireModels.MODEL[type], me.lat, me.lon, me.alt);
414 trace("Created 3d model " ~ type ~ " " ~ CAFireModels.MODEL[type]);
417 ############################################################
419 if (me.model != nil) me.model.remove();
422 ############################################################
425 ###############################################################################
426 # Singleton that maintains the CA models.
427 var CAFireModels = {};
429 CAFireModels.MODEL = { # Model paths
430 "fire" : "Models/Effects/Wildfire/wildfire.xml",
431 "soot" : "Models/Effects/Wildfire/soot.xml",
432 "foam" : "Models/Effects/Wildfire/foam.xml",
434 "retardant" : "Models/Effects/Wildfire/retardant.xml",
439 CAFireModels.grid = {}; # Sparse cell model grid storage.
440 CAFireModels.pending = []; # List of pending model changes.
441 CAFireModels.models_enabled = 1;
442 CAFireModels.loopid = 0;
443 ######################################################################
445 ############################################################
446 CAFireModels.init = func {
448 setlistener(models_enabled_pp, func (n) {
449 me.set_models_enabled(n.getValue());
453 ############################################################
454 # Reset the model grid to the empty state.
455 CAFireModels.reset = func (enabled) {
456 # Clear the model grid.
457 foreach (var x; keys(me.grid)) {
458 foreach (var y; keys(me.grid[x])) {
459 if (me.grid[x][y] != nil) me.grid[x][y].remove();
470 ############################################################
471 # Start the CA model grid.
472 CAFireModels.start = func {
474 me._loop_(me.loopid);
476 ############################################################
477 # Stop the CA model grid.
478 # Note that it will catch up lost time when started again.
479 CAFireModels.stop = func {
482 ############################################################
483 # Add a new cell model.
484 CAFireModels.add = func(x, y, alt) {
485 append(me.pending, { x: x, y: y, alt: alt });
487 ############################################################
488 # Update a cell model.
489 CAFireModels.set_type = func(x, y, type) {
490 append(me.pending, { x: x, y: y, type: type });
492 ############################################################
493 CAFireModels.set_models_enabled = func(on=1) {
494 me.models_enabled = on;
495 # We should do a pass over all cells here to add/remove models.
496 # For now I don't so only active cells will actually remove the
497 # models. All models will be hidden by their select animations, though.
499 ######################################################################
501 ############################################################
502 CAFireModels.update = func {
503 var work = size(me.pending)/10;
504 while (size(me.pending) > 0 and work > 0) {
505 var c = me.pending[0];
506 me.pending = subvec(me.pending, 1);
508 if (contains(c, "alt")) {
509 if (me.grid[c.x] == nil) {
512 me.grid[c.x][c.y] = CellModel.new(c.x, c.y, c.alt);
514 if (contains(c, "type")) {
515 me.grid[c.x][c.y].set_type(c.type);
519 ############################################################
520 CAFireModels._loop_ = func(id) {
521 id == me.loopid or return;
523 settimer(func { me._loop_(id); }, 0);
525 ###############################################################################
527 ###############################################################################
528 # Singleton that maintains the fire cell CA grid.
531 CAFire.CELL_SIZE = 0.03; # "nm" (or rather minutes)
532 CAFire.GENERATION_DURATION = 4.0; # seconds
533 CAFire.PASSES = 8.0; # Passes per full update.
534 CAFire.IGNITE_THRESHOLD = 0.3; # Minimum cell state for igniting neighbours.
535 CAFire.grid = {}; # Sparse cell grid storage.
536 CAFire.generation = 0; # CA generation. Defined from the epoch.
538 CAFire.active = []; # List of active cells. These will be updated.
539 CAFire.old = 0; # selects new/old cell state.
540 CAFire.next = 1; # selects new/old cell state.
541 CAFire.cells_created = 0;
542 CAFire.cells_burning = 0;
543 CAFire.pass = 0; # Update pass within the current full update.
544 CAFire.pass_work = 0; # Cells to update in each pass.
545 CAFire.remaining_work = []; # Work remaining in this full update.
547 CAFire.event_log = []; # List of all events that has occured so far.
548 CAFire.load_count = 0;
549 CAFire.BURN_RATE = { # Burn rate DB. grid widths per second
552 "grass_rwy" : 0.0010,
553 "ShrubGrassCover" : 0.0010,
554 "ScrubCover" : 0.0010,
555 "BareTundraCover" : 0.0010,
556 "MixedTundraCover" : 0.0010,
557 "HerbTundraCover" : 0.0010,
558 "MixedCropPastureCover" : 0.0010,
559 "DryCropPastureCover" : 0.0010,
560 "CropGrassCover" : 0.0010,
561 "CropWoodCover" : 0.0010,
563 "DeciduousBroadCover" : 0.0005,
564 "EvergreenBroadCover" : 0.0005,
565 "MixedForestCover" : 0.0005,
566 "EvergreenNeedleCover" : 0.0005,
567 "WoodedTundraCover" : 0.0005,
568 "DeciduousNeedleCover" : 0.0005,
570 "BuiltUpCover" : 0.0005,
574 CAFire.NEIGHBOURS = # Neighbour index offsets. First row and column
576 [[[-1, 0], [0, 1], [1, 0], [0, -1]],
577 [[-1, 1], [1, 1], [1, -1], [-1, -1]]];
578 ######################################################################
580 ############################################################
583 me.reset(1, SimTime.current_time());
585 ############################################################
586 # Reset the CA to the empty state and set its current time to sim_time.
587 CAFire.reset = func (enabled, sim_time) {
588 # Clear the model grid.
589 CAFireModels.reset(enabled);
592 me.generation = int(sim_time/CAFire.GENERATION_DURATION);
596 me.cells_created = 0;
597 me.cells_burning = 0;
600 me.remaining_work = [];
603 me.enabled = enabled;
610 ############################################################
612 CAFire.start = func {
613 CAFireModels.start();
616 me._loop_(me.loopid);
618 ############################################################
619 # Stop the CA. Note that it will catch up lost time when started again.
625 ############################################################
626 # Start a fire in the cell at pos.
627 CAFire.ignite = func (lat, lon) {
628 trace("CAFire.ignite: Fire at " ~ lat ~", " ~ lon ~ ".");
629 var x = int(lon*60/me.CELL_SIZE);
630 var y = int(lat*60/me.CELL_SIZE);
631 var cell = me.get_cell(x, y);
633 cell = FireCell.new(x, y);
637 if (cell != nil) cell.ignite();
638 append(me.event_log, [SimTime.current_time(), "ignite", lat, lon]);
640 ############################################################
641 # Resolve a water drop.
642 # For now: Assume that water makes the affected cell nonflammable forever
643 # and extinguishes it if burning.
644 # radius - meter : double
645 # Note: volume is unused ATM.
646 CAFire.resolve_water_drop = func (lat, lon, radius, volume=0) {
647 trace("CAFire.resolve_water_drop: Dumping water at " ~ lat ~", " ~ lon ~
648 " radius " ~ radius ~".");
649 var x = int(lon*60/me.CELL_SIZE);
650 var y = int(lat*60/me.CELL_SIZE);
651 var r = int(2*radius/(me.CELL_SIZE*1852.0));
652 var result = { extinguished : 0, protected : 0, waste : 0 };
653 for (var dx = -r; dx <= r; dx += 1) {
654 for (var dy = -r; dy <= r; dy += 1) {
655 var cell = me.get_cell(x + dx, y + dy);
657 cell = FireCell.new(x + dx, y + dy);
658 me.set_cell(x + dx, y + dy,
662 var res = cell.extinguish("water");
664 result.extinguished += 1;
666 if (res == 0) result.protected += 1;
667 else result.waste += 1;
675 [SimTime.current_time(), "water_drop", lat, lon, radius]);
678 ############################################################
679 # Resolve a fire retardant drop.
680 # For now: Assume that the retardant makes the affected cell nonflammable
681 # forever and extinguishes it if burning.
682 # Note: volume is unused ATM.
683 CAFire.resolve_retardant_drop = func (lat, lon, radius, volume=0) {
684 trace("CAFire.resolve_retardant_drop: Dumping retardant at " ~
685 lat ~", " ~ lon ~ " radius " ~ radius ~".");
686 var x = int(lon*60/me.CELL_SIZE);
687 var y = int(lat*60/me.CELL_SIZE);
688 var r = int(2*radius/(me.CELL_SIZE*1852.0));
689 var result = { extinguished : 0, protected : 0, waste : 0 };
690 for (var dx = -r; dx <= r; dx += 1) {
691 for (var dy = -r; dy <= r; dy += 1) {
692 var cell = me.get_cell(x + dx, y + dy);
694 cell = FireCell.new(x + dx, y + dy);
695 me.set_cell(x + dx, y + dy,
699 var res = cell.extinguish("retardant");
701 result.extinguished += 1;
703 if (res == 0) result.protected += 1;
704 else result.waste += 1;
712 [SimTime.current_time(), "retardant_drop", lat, lon, radius]);
715 ############################################################
716 # Resolve a foam drop.
717 # For now: Assume that water makes the affected cell nonflammable forever
718 # and extinguishes it if burning.
719 # radius - meter : double
720 # Note: volume is unused ATM.
721 CAFire.resolve_foam_drop = func (lat, lon, radius, volume=0) {
722 trace("CAFire.resolve_foam_drop: Dumping foam at " ~ lat ~", " ~ lon ~
723 " radius " ~ radius ~".");
724 var x = int(lon*60/me.CELL_SIZE);
725 var y = int(lat*60/me.CELL_SIZE);
726 var r = int(2*radius/(me.CELL_SIZE*1852.0));
727 var result = { extinguished : 0, protected : 0, waste : 0 };
728 for (var dx = -r; dx <= r; dx += 1) {
729 for (var dy = -r; dy <= r; dy += 1) {
730 var cell = me.get_cell(x + dx, y + dy);
732 cell = FireCell.new(x + dx, y + dy);
733 me.set_cell(x + dx, y + dy,
737 var res = cell.extinguish("foam");
739 result.extinguished += 1;
741 if (res == 0) result.protected += 1;
742 else result.waste += 1;
750 [SimTime.current_time(), "foam_drop", lat, lon, radius]);
753 ############################################################
754 # Save the current event log.
755 # This is modelled on Melchior FRANZ's ac_state.nas.
756 CAFire.save_event_log = func (filename) {
757 var args = props.Node.new({ filename : filename });
758 var data = args.getNode("data", 1);
760 gui.popupTip("Wildfire: Saving state to " ~ filename);
763 foreach (var e; me.event_log) {
764 var event = data.getNode("event[" ~ i ~ "]", 1);
765 event.getNode("time-sec", 1).setDoubleValue(e[0]);
766 event.getNode("type", 1).setValue(e[1]);
767 event.getNode("latitude", 1).setDoubleValue(e[2]);
768 event.getNode("longitude", 1).setDoubleValue(e[3]);
769 # Event type specific data.
770 if (e[1] == "water_drop")
771 event.getNode("radius", 1).setDoubleValue(e[4]);
772 if (e[1] == "foam_drop")
773 event.getNode("radius", 1).setDoubleValue(e[4]);
774 if (e[1] == "retardant_drop")
775 event.getNode("radius", 1).setDoubleValue(e[4]);
780 # Add save event to aid skip ahead restore.
781 var event = data.getNode("event[" ~ i ~ "]", 1);
782 event.getNode("time-sec", 1).setDoubleValue(SimTime.current_time());
783 event.getNode("type", 1).setValue("save");
785 fgcommand("savexml", args);
787 ############################################################
789 # skip_ahead_until - skip from last event to this time : double (epoch)
790 # fast forward from skip_ahead_until
792 # x < last event - fast forward all the way to current time (use 0).
793 # -1 - skip to current time.
794 CAFire.load_event_log = func (filename, skip_ahead_until) {
796 var logbase = "/tmp/wildfire-load-log[" ~ me.load_count ~ "]";
797 if (!fgcommand("loadxml",
798 props.Node.new({ filename : filename,
799 targetnode : logbase }))) {
800 printlog("alert", "Wildfire ... failed loading '" ~ filename ~ "'");
804 # Fast forward the automaton from the first logged event to the current time.
805 CAFireModels.set_models_enabled(0);
807 var events = props.globals.getNode(logbase).getChildren("event");
808 foreach (var event; events) {
811 me.reset(1, event.getNode("time-sec").getValue());
814 # event.getNode("time-sec").getValue() ~ "," ~
815 # event.getNode("type").getValue() ~ "]");
816 var e = [event.getNode("time-sec").getValue(),
817 event.getNode("type").getValue()];
819 # Fast forward state.
820 while (me.generation * me.GENERATION_DURATION < e[0]) {
821 # print("between event ff " ~ me.generation);
824 # Apply event. Note: The logged time is wrong ATM.
825 if (event.getNode("type").getValue() == "ignite") {
826 me.ignite(event.getNode("latitude").getValue(),
827 event.getNode("longitude").getValue());
828 me.event_log[size(me.event_log) - 1][0] = e[0];
830 if (event.getNode("type").getValue() == "water_drop") {
831 me.resolve_water_drop(event.getNode("latitude").getValue(),
832 event.getNode("longitude").getValue(),
833 event.getNode("radius").getValue());
834 me.event_log[size(me.event_log) - 1][0] = e[0];
836 if (event.getNode("type").getValue() == "foam_drop") {
837 me.resolve_foam_drop(event.getNode("latitude").getValue(),
838 event.getNode("longitude").getValue(),
839 event.getNode("radius").getValue());
840 me.event_log[size(me.event_log) - 1][0] = e[0];
842 if (event.getNode("type").getValue() == "retardant_drop") {
843 me.resolve_retardant_drop(event.getNode("latitude").getValue(),
844 event.getNode("longitude").getValue(),
845 event.getNode("radius").getValue());
846 me.event_log[size(me.event_log) - 1][0] = e[0];
850 me.reset(1, SimTime.current_time());
854 var now = SimTime.current_time();
855 if (skip_ahead_until == -1) {
856 me.generation = int(now/me.GENERATION_DURATION);
858 if (me.generation < int(skip_ahead_until/me.GENERATION_DURATION)) {
859 me.generation = int(skip_ahead_until/me.GENERATION_DURATION);
861 # Catch up with current time. NOTE: This can be very time consuming!
862 while (me.generation * me.GENERATION_DURATION < now)
865 CAFireModels.set_models_enabled(getprop(models_enabled_pp));
867 ######################################################################
868 # Internal operations
869 CAFire.get_cell = func (x, y) {
870 if (me.grid[x] == nil) me.grid[x] = {};
871 return me.grid[x][y];
873 ############################################################
874 CAFire.set_cell = func (x, y, cell) {
875 if (me.grid[x] == nil) {
878 me.grid[x][y] = cell;
880 ############################################################
881 CAFire.update = func {
882 if (!me.enabled) return; # The CA is disabled.
883 if (me.pass == me.PASSES) {
884 # Setup a new main iteration.
887 me.remaining_work = me.active;
889 me.pass_work = size(me.remaining_work)/ me.PASSES + 1;
897 if (me.cells_burning > 0) {
899 "Wildfire: generation " ~ me.generation ~ " updating " ~
900 size(me.remaining_work) ~" / " ~ me.cells_created ~
901 " created cells. " ~ me.cells_burning ~ " burning cells.");
904 if (LOD_Low <= me.cells_burning) {
905 props.globals.getNode(fire_LOD_pp).setIntValue(1);
906 props.globals.getNode(smoke_LOD_pp).setIntValue(1);
908 if ((LOD_High <= me.cells_burning) and (me.cells_burning < LOD_Low)) {
909 props.globals.getNode(fire_LOD_pp).setIntValue(5);
910 props.globals.getNode(smoke_LOD_pp).setIntValue(5);
912 if (me.cells_burning < LOD_High) {
913 props.globals.getNode(fire_LOD_pp).setIntValue(10);
914 props.globals.getNode(smoke_LOD_pp).setIntValue(10);
916 me.cells_burning = 0;
921 var work = me.pass_work;
922 var c = pop(me.remaining_work);
924 if (c.update() != 0) {
925 append(me.active, c);
926 me.cells_burning += c.burning[me.next];
929 if (work <= 0) return;
930 c = pop(me.remaining_work);
933 ############################################################
934 CAFire._loop_ = func(id) {
935 id == me.loopid or return;
937 settimer(func { me._loop_(id); },
938 me.GENERATION_DURATION * (me.generation + 1/me.PASSES) -
939 SimTime.current_time());
941 ###############################################################################
943 ###############################################################################
944 # Main initialization.
947 _setlistener("/sim/signals/nasal-dir-initialized", func {
949 Binary = mp_broadcast.Binary;
951 # Create configuration properties if they don't exist already.
952 props.globals.initNode(CA_enabled_pp, 1, "BOOL");
953 setlistener(CA_enabled_pp, func (n) {
954 if (getprop("/sim/signals/reinit")) return; # Ignore resets.
955 CAFire.reset(n.getValue(), SimTime.current_time());
957 props.globals.initNode(MP_share_pp, 1, "BOOL");
958 props.globals.initNode(crash_fire_pp, 1, "BOOL");
959 props.globals.initNode(impact_fire_pp, 1, "BOOL");
960 props.globals.initNode(save_on_exit_pp, 0, "BOOL");
961 props.globals.initNode(restore_on_startup_pp, 0, "BOOL");
962 props.globals.initNode(models_enabled_pp, 1, "BOOL");
963 props.globals.initNode(report_score_pp, 1, "BOOL");
964 props.globals.initNode(event_file_pp, "", "STRING");
965 props.globals.initNode(time_hack_pp, "", "STRING");
967 props.globals.initNode(fire_LOD_pp, 10, "INT");
968 props.globals.initNode(smoke_LOD_pp, 10, "INT");
972 mp_broadcast.BroadcastChannel.new(msg_channel_mpp, parse_msg);
975 # Start the score reporting.
976 settimer(score_report_loop, CAFire.GENERATION_DURATION);
978 setlistener("/sim/signals/exit", func {
979 if (getprop(report_score_pp) and (CAFire.cells_created > 0))
981 if (getprop(save_on_exit_pp))
982 CAFire.save_event_log(SAVEDIR ~ "fire_log.xml");
985 # Determine the skip-ahead-to time, if any.
986 var time_hack = time_string_to_epoch(getprop(time_hack_pp));
987 if (time_hack > SimTime.current_time()) {
989 "wildfire.nas: Ignored time hack " ~
990 (SimTime.current_time() - time_hack) ~
991 " seconds into the future.");
992 # Skip ahead to current time instead.
994 } elsif (time_hack > 0) {
996 "wildfire.nas: Time hack " ~
997 (SimTime.current_time() - time_hack) ~
1000 # Skip ahead to current time instead.
1004 if (getprop(event_file_pp) != "") {
1006 # Delay loading the log until the terrain is there. Note: hack.
1007 CAFire.load_event_log(getprop(event_file_pp), time_hack);
1009 } elsif (getprop(restore_on_startup_pp)) {
1011 # Delay loading the log until the terrain is there. Note: hack.
1012 # Restore skips ahead to current time.
1013 CAFire.load_event_log(SAVEDIR ~ "fire_log.xml", -1);
1017 # Detect aircraft crash.
1018 setlistener("sim/crashed", func(n) {
1019 if (getprop(crash_fire_pp) and n.getBoolValue())
1020 wildfire.ignite(geo.aircraft_position());
1024 var impact_node = props.globals.getNode("sim/ai/aircraft/impact/bomb", 1);
1025 setlistener("sim/ai/aircraft/impact/bomb", func(n) {
1027 if (getprop(impact_fire_pp) and n.getBoolValue()){
1028 var node = props.globals.getNode(n.getValue(), 1);
1029 var impactpos = geo.Coord.new();
1030 impactpos.set_latlon
1031 (node.getNode("impact/latitude-deg").getValue(),
1032 node.getNode("impact/longitude-deg").getValue());
1033 wildfire.ignite(impactpos);
1038 printlog("info", "Wildfire ... initialized.");
1040 ###############################################################################
1042 ###############################################################################
1045 # Convert a time string in the format yyyy[:mm[:dd[:hh[:mm[:ss]]]]]
1046 # to seconds since 1970:01:01:00:00:00.
1048 # Note: This is an over simplified approximation.
1049 var time_string_to_epoch = func (time) {
1051 if (string.scanf(time, "%d:%d:%d:%d:%d:%d", var res1 = []) != 0) {
1053 } elsif (string.scanf(time, "%d:%d:%d:%d:%d", var res2 = []) != 0) {
1055 } elsif (string.scanf(time, "%d:%d:%d:%d", var res3 = []) != 0) {
1056 res = res3 ~ [0, 0];
1057 } elsif (string.scanf(time, "%d:%d:%d", var res4 = []) != 0) {
1058 res = res4 ~ [0, 0, 0];
1059 } elsif (string.scanf(time, "%d:%d", var res5 = []) != 0) {
1060 res = res5 ~ [0, 0, 0, 0];
1061 } elsif (string.scanf(time, "%d", var res6 = []) != 0) {
1062 res = res6 ~ [0, 0, 0, 0, 0];
1067 (res[0] - 1970) * 3.15569e7 +
1068 (res[1] - 1) * 2.63e+6 +
1069 (res[2] - 1) * 86400 +
1076 ###############################################################################
1077 ## WildFire configuration dialog.
1078 ## Partly based on Till Bush's multiplayer dialog
1083 #################################################################
1084 init : func (x = nil, y = nil) {
1087 me.bg = [0, 0, 0, 0.3]; # background color
1088 me.fg = [[1.0, 1.0, 1.0, 1.0]];
1091 me.title = "Wildfire";
1092 me.basenode = props.globals.getNode("/environment/wildfire");
1094 me.namenode = props.Node.new({"dialog-name" : me.title });
1097 #################################################################
1099 if (me.dialog != nil)
1102 me.dialog = gui.Widget.new();
1103 me.dialog.set("name", me.title);
1105 me.dialog.set("x", me.x);
1107 me.dialog.set("y", me.y);
1109 me.dialog.set("layout", "vbox");
1110 me.dialog.set("default-padding", 0);
1111 var titlebar = me.dialog.addChild("group");
1112 titlebar.set("layout", "hbox");
1113 titlebar.addChild("empty").set("stretch", 1);
1114 titlebar.addChild("text").set("label", "Wildfire settings");
1115 titlebar.addChild("empty").set("stretch", 1);
1116 var w = titlebar.addChild("button");
1117 w.set("pref-width", 16);
1118 w.set("pref-height", 16);
1119 w.set("legend", "");
1120 w.set("default", 0);
1121 w.setBinding("nasal", "wildfire.dialog.destroy(); ");
1122 w.setBinding("dialog-close");
1123 me.dialog.addChild("hrule");
1125 var content = me.dialog.addChild("group");
1126 content.set("layout", "vbox");
1127 content.set("halign", "center");
1128 content.set("default-padding", 5);
1130 foreach (var b; [["Enabled", CA_enabled_pp],
1131 ["Share over MP", MP_share_pp],
1132 ["Show 3d models", models_enabled_pp],
1133 ["Crash starts fire", crash_fire_pp],
1134 ["Impact starts fire", impact_fire_pp],
1135 ["Report score", report_score_pp],
1136 ["Save on exit", save_on_exit_pp]]) {
1137 var w = content.addChild("checkbox");
1138 w.node.setValues({"label" : b[0],
1140 "property" : b[1]});
1141 w.setBinding("nasal",
1142 "setprop(\"" ~ b[1] ~ "\"," ~
1143 "!getprop(\"" ~ b[1] ~ "\"))");
1145 me.dialog.addChild("hrule");
1148 var buttons = me.dialog.addChild("group");
1149 buttons.node.setValues({"layout" : "hbox"});
1152 var load = buttons.addChild("button");
1153 load.node.setValues({"legend" : "Load Wildfire log",
1154 "halign" : "center"});
1155 load.setBinding("nasal",
1156 "wildfire.dialog.select_and_load()");
1159 var close = buttons.addChild("button");
1160 close.node.setValues({"legend" : "Close",
1163 close.setBinding("nasal", "wildfire.dialog.destroy();");
1164 close.setBinding("dialog-close");
1166 fgcommand("dialog-new", me.dialog.prop());
1167 fgcommand("dialog-show", me.namenode);
1169 #################################################################
1171 fgcommand("dialog-close", me.namenode);
1173 #################################################################
1177 foreach(var l; me.listeners)
1179 delete(gui.dialog, "\"" ~ me.title ~ "\"");
1181 #################################################################
1189 #################################################################
1190 select_and_load : func {
1191 var selector = gui.FileSelector.new
1192 (func (n) { load_event_log(n.getValue(), -1); },
1193 "Load Wildfire log", # dialog title
1194 "Load", # button text
1195 ["*.xml"], # pattern for files
1196 SAVEDIR, # start dir
1197 "fire_log.xml"); # default file name
1201 ###############################################################################