Phi: nicer scroll animation for METAR widget
[fg:fgdata.git] / Nasal / wildfire.nas
1 ###############################################################################
2 ##
3 ##  A cellular automaton forest fire model with the ability to
4 ##  spread over the multiplayer network.
5 ##
6 ##  Copyright (C) 2007 - 2012  Anders Gidenstam  (anders(at)gidenstam.org)
7 ##  This file is licensed under the GPL license version 2 or later.
8 ##
9 ###############################################################################
10
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.
16
17 # Set this to print for debug.
18 var trace = func {}
19
20 # Where to save fire event logs.
21 var SAVEDIR = getprop("/sim/fg-home") ~ "/Wildfire/";
22
23 # Maximum number of ignite events a single user can send per second.
24 var MAX_IGNITE_RATE = 0.25;
25
26 ###############################################################################
27 ## External API
28
29 # Start a fire.
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());
36 }
37
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));
46   }
47   var res = CAFire.resolve_water_drop(pos.lat(), pos.lon(), radius, volume);
48   if (source) {
49     score.extinguished += res.extinguished;
50     score.protected    += res.protected;
51     score.waste        += res.waste;
52   }
53 }
54
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));
63   }
64   var res = CAFire.resolve_retardant_drop(pos.lat(), pos.lon(),
65                                           radius, volume);
66   if (source) {
67     score.extinguished += res.extinguished;
68     score.protected    += res.protected;
69     score.waste        += res.waste;
70   }
71 }
72
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));
81   }
82   var res = CAFire.resolve_foam_drop(pos.lat(), pos.lon(),
83                                      radius, volume);
84   if (source) {
85     score.extinguished += res.extinguished;
86     score.protected    += res.protected;
87     score.waste        += res.waste;
88   }
89 }
90
91 # Load an event log.
92 #   skip_ahead_until - skip from last event to this time : double (epoch)
93 #                      fast forward from skip_ahead_until
94 #                      to current time.
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);
100 }
101
102 # Save an event log.
103 #
104 var save_event_log = func (filename) {
105   CAFire.save_event_log(filename);
106 }
107
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);
115 }
116
117 ###############################################################################
118 # Internals.
119 ###############################################################################
120
121 var msg_channel_mpp = "environment/wildfire/data";
122 var broadcast = nil;
123 var seq = 0;
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";
139 var LOD_High = 20;
140 var LOD_Low  = 50;
141 var mp_last_limited_event = {}; # source : time
142
143 var score = { extinguished : 0, protected : 0, waste : 0 };
144 var old_score = { extinguished : 0, protected : 0, waste : 0 };
145
146 ###############################################################################
147 # Utility functions.
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) ~
154               " fire cells.");
155     }
156     old_score.extinguished = score.extinguished;
157     old_score.protected    = score.protected;
158     old_score.waste        = score.waste;
159   } else {
160     if (getprop(report_score_pp) and (score.waste > old_score.waste))
161       setprop("/sim/messages/copilot",
162               "Miss!");
163     old_score.extinguished = score.extinguished;
164     old_score.protected    = score.protected;
165     old_score.waste        = score.waste;
166   }
167   settimer(score_report_loop, CAFire.GENERATION_DURATION);
168 }
169
170 ###############################################################################
171 # MP messages
172
173 var ignition_msg = func (pos) {
174   seq += 1;
175   return Binary.encodeInt(seq) ~ Binary.encodeByte(1) ~
176       Binary.encodeCoord(pos);
177 }
178
179 var water_drop_msg = func (pos, radius, volume) {
180   seq += 1;
181   return Binary.encodeInt(seq) ~ Binary.encodeByte(2) ~
182       Binary.encodeCoord(pos) ~ Binary.encodeDouble(radius);
183 }
184
185 var retardant_drop_msg = func (pos, radius, volume) {
186   seq += 1;
187   return Binary.encodeInt(seq) ~ Binary.encodeByte(3) ~
188       Binary.encodeCoord(pos) ~ Binary.encodeDouble(radius);
189 }
190
191 var foam_drop_msg = func (pos, radius, volume) {
192   seq += 1;
193   return Binary.encodeInt(seq) ~ Binary.encodeByte(4) ~
194       Binary.encodeCoord(pos) ~ Binary.encodeDouble(radius);
195 }
196
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));
201   if (type == 1) {
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));
206       ignite(pos, 0);
207     } else {
208       printlog("alert", "wildfire.nas: Ignored ignite event flood from " ~
209                source.getNode("callsign").getValue());
210     }
211     mp_last_limited_event[i] = cur_time;
212   }
213   if (type == 2) {
214     var pos    = Binary.decodeCoord(substr(msg, 6));
215     var radius = Binary.decodeDouble(substr(msg, 36));
216     resolve_water_drop(pos, radius, 0, 0);
217   }
218   if (type == 3) {
219     var pos    = Binary.decodeCoord(substr(msg, 6));
220     var radius = Binary.decodeDouble(substr(msg, 36));
221     resolve_retardant_drop(pos, radius, 0, 0);
222   }
223   if (type == 4) {
224     var pos    = Binary.decodeCoord(substr(msg, 6));
225     var radius = Binary.decodeDouble(substr(msg, 36));
226     resolve_foam_drop(pos, radius, 0, 0);
227   }
228 }
229
230 ###############################################################################
231 # Simulation time management.
232 # NOTE: Time warp is ignored for the time being.
233
234 var SimTime = {
235 ############################################################
236   init : func {
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");
240   },
241   current_time : func {
242     return me.real_time_base + me.elapsed_time.getValue();
243   }
244 };
245
246
247 ###############################################################################
248 #  Class that maintains the state of one fire cell.
249 var FireCell = {
250 ############################################################
251   new : func (x, y) {
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;
256     m.x   = x;
257     m.y   = y;
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.
261
262     # Fetch ground type.
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;
266     m.alt        = geo_info[0];
267     m.burn_rate  = 0.0;
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];
273       }
274     }
275     CAFireModels.add(x, y, m.alt);
276     append(CAFire.active, m);
277     CAFire.cells_created += 1;
278     return m;
279   },
280 ############################################################
281   ignite : func {
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;
289     } else {
290       trace("FireCell[" ~ me.lat ~ "," ~me.lon ~ "] Failed to ignite!");
291     }
292   },
293 ############################################################
294   extinguish : func (type="soot") {
295     trace("FireCell[" ~ me.x ~ "," ~ me.y ~ "] extinguished.");
296     var result = 0;
297     if (me.burning[CAFire.old]) result = 1;
298     if (me.burn_rate == 0) result = -1; # A waste to protect this cell.
299
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");
307     } else {
308       # Use a model representing contamination here.
309       CAFireModels.set_type(me.x, me.y, type);
310     }
311     return result;
312   },
313 ############################################################
314   update : func () {
315     trace("FireCell[" ~ me.x ~ "," ~me.y ~ "] " ~ me.state[CAFire.old]);
316     if ((me.state[CAFire.old] == 1) and (me.burning[CAFire.old] == 0))
317       return 0;
318     if ((me.burn_rate == 0) and (me.burning[CAFire.old] == 0))
319       return 0;
320     if (me.last >= CAFire.generation) return 1; # Some event has happened here.
321     me.last = CAFire.generation;
322
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;
327
328     if ((me.burning[CAFire.old] == 0) and
329         (0 < me.state[CAFire.next]) and (me.state[CAFire.old] < 1)) {
330       me.ignite();
331       return 1;
332     }
333     if (me.state[CAFire.next] >= 1) {
334       me.extinguish("soot");
335       return 0;
336     }
337     if (me.burn_rate == 0) {
338       # Does this make sense?
339       me.extinguish("protected");
340       return 0;
341     }
342     me.burning[CAFire.next] = me.burning[CAFire.old];
343     CAFireModels.set_type(me.x, me.y, me.burning[CAFire.old] ? "fire" : "soot");
344     return 1;
345   },
346 ############################################################
347 # Get neightbour burn values.
348   get_neighbour_burn : func (create) {
349     var burn = 0.0;
350     foreach (var d; CAFire.NEIGHBOURS[0]) {
351       var c = CAFire.get_cell(me.x + d[0], me.y + d[1]);
352       if (c != nil) {
353         burn += c.burning[CAFire.old] * c.burn_rate *
354                 (5*me.alt / c.alt) *
355                 c.state[CAFire.old] * CAFire.GENERATION_DURATION;
356       } else {
357         if (create) {
358           # Create the neighbour.
359           CAFire.set_cell(me.x + d[0], me.y + d[1],
360                           FireCell.new(me.x + d[0],
361                                        me.y + d[1]));
362         }
363       }
364     }
365     foreach (var d; CAFire.NEIGHBOURS[1]) {
366       var c = CAFire.get_cell(me.x + d[0], me.y + d[1]);
367       if (c != nil) {
368         burn += 0.785 * c.burning[CAFire.old] * c.burn_rate *
369                 (5*me.alt / c.alt) *
370                 c.state[CAFire.old] * CAFire.GENERATION_DURATION;
371       } else {
372         if (create) {
373           # Create the neighbour.
374           CAFire.set_cell(me.x + d[0], me.y + d[1],
375                           FireCell.new(me.x + d[0],
376                                        me.y + d[1]));
377         }
378       }
379     }
380     return burn;
381   },
382 ############################################################
383 };
384
385 ###############################################################################
386 #  Class that maintains the 3d model(s) for one fire cell.
387 var CellModel = {
388 ############################################################
389     new : func (x, y, alt) {
390         var m = { parents: [CellModel] };
391         m.type  = "none";
392         m.model = nil;
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;
395         m.x   = x;
396         m.y   = y;
397         m.alt = alt + 0.1;
398         return m;
399     },
400 ############################################################
401     set_type : func(type) {
402         if (me.model != nil) {
403             if (me.type == type) return;
404             me.model.remove();
405             me.model = nil;
406         }
407         me.type = type;
408         if (CAFireModels.MODEL[type] == "") return;
409
410         # Always put "cheap" models for now.
411         if (CAFireModels.models_enabled or (type != "fire")) {
412             me.model =
413                 geo.put_model(CAFireModels.MODEL[type], me.lat, me.lon, me.alt);
414             trace("Created 3d model " ~ type ~ " " ~ CAFireModels.MODEL[type]);
415         }
416     },
417 ############################################################
418     remove : func() {
419         if (me.model != nil) me.model.remove();
420         me.model = nil;
421     }
422 ############################################################
423 };
424
425 ###############################################################################
426 #  Singleton that maintains the CA models.
427 var CAFireModels = {};
428 # Constants
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",
433     "water"                  : "",
434     "retardant"              : "Models/Effects/Wildfire/retardant.xml",
435     "protected"              : "",
436     "none"                   : "",
437 };
438 # State
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 ######################################################################
444 # Public operations
445 ############################################################
446 CAFireModels.init = func {
447   # Initialization.
448   setlistener(models_enabled_pp, func (n) {
449     me.set_models_enabled(n.getValue());
450   }, 1);
451   me.reset(1);
452 }
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();
460     }
461   }
462   # Reset state.
463   me.grid = {};
464   me.pending = [];
465
466   if (enabled) {
467     me.start();
468   }
469 }
470 ############################################################
471 # Start the CA model grid.
472 CAFireModels.start = func {
473   me.loopid += 1;
474   me._loop_(me.loopid);
475 }
476 ############################################################
477 # Stop the CA model grid.
478 # Note that it will catch up lost time when started again.
479 CAFireModels.stop = func {
480   me.loopid += 1;
481 }
482 ############################################################
483 # Add a new cell model.
484 CAFireModels.add = func(x, y, alt) {
485   append(me.pending, { x: x, y: y, alt: alt });
486 }
487 ############################################################
488 # Update a cell model.
489 CAFireModels.set_type = func(x, y, type) {
490   append(me.pending, { x: x, y: y, type: type });
491 }
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.
498 }
499 ######################################################################
500 # Private operations
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);
507     work -= 1;
508     if (contains(c, "alt")) {
509       if (me.grid[c.x] == nil) {
510         me.grid[c.x] = {};
511       }
512       me.grid[c.x][c.y] = CellModel.new(c.x, c.y, c.alt);
513     }
514     if (contains(c, "type")) {
515       me.grid[c.x][c.y].set_type(c.type);
516     }
517   }
518 }
519 ############################################################
520 CAFireModels._loop_ = func(id) {
521   id == me.loopid or return;
522   me.update();
523   settimer(func { me._loop_(id); }, 0);
524 }
525 ###############################################################################
526
527 ###############################################################################
528 #  Singleton that maintains the fire cell CA grid.
529 var CAFire = {};
530 # State
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.
537 CAFire.enabled = 0;
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.
546 CAFire.loopid = 0;
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
550 # Grass
551     "Grass"                 : 0.0010,
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,
562 # Forest
563     "DeciduousBroadCover"   : 0.0005,
564     "EvergreenBroadCover"   : 0.0005,
565     "MixedForestCover"      : 0.0005,
566     "EvergreenNeedleCover"  : 0.0005,
567     "WoodedTundraCover"     : 0.0005,
568     "DeciduousNeedleCover"  : 0.0005,
569 # City
570     "BuiltUpCover"          : 0.0005,
571 # ?
572     "Landmass"              : 0.0005
573 };
574 CAFire.NEIGHBOURS =      # Neighbour index offsets. First row and column
575                          # and then diagonal.
576     [[[-1, 0], [0, 1], [1, 0], [0, -1]],
577      [[-1, 1], [1, 1], [1, -1], [-1, -1]]];
578 ######################################################################
579 # Public operations
580 ############################################################
581 CAFire.init = func {
582   # Initialization.
583   me.reset(1, SimTime.current_time());
584 }
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);
590   # Reset state.
591   me.grid = {};
592   me.generation = int(sim_time/CAFire.GENERATION_DURATION);
593   me.active = [];
594   me.old  = 0;
595   me.next = 1;
596   me.cells_created = 0;
597   me.cells_burning = 0;
598   me.pass = 0;
599   me.pass_work = 0;
600   me.remaining_work = [];
601   me.event_log = [];
602
603   me.enabled = enabled;
604   if (me.enabled) {
605     me.start();
606   } else {
607     me.stop();
608   }
609 }
610 ############################################################
611 # Start the CA.
612 CAFire.start = func {
613   CAFireModels.start();
614   broadcast.start();
615   me.loopid += 1;
616   me._loop_(me.loopid);
617 }
618 ############################################################
619 # Stop the CA. Note that it will catch up lost time when started again.
620 CAFire.stop = func {
621   CAFireModels.stop();
622   broadcast.stop();
623   me.loopid += 1;
624 }
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);
632   if (cell == nil) {
633     cell = FireCell.new(x, y);
634     me.set_cell(x, y,
635                 cell);
636   }
637   if (cell != nil) cell.ignite();
638   append(me.event_log, [SimTime.current_time(), "ignite", lat, lon]);
639 }
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);
656       if (cell == nil) {
657         cell = FireCell.new(x + dx, y + dy);
658         me.set_cell(x + dx, y + dy,
659                     cell);
660       }
661       if (cell != nil) {
662         var res = cell.extinguish("water");
663         if (res > 0) {
664           result.extinguished += 1;
665         } else {
666           if (res == 0) result.protected += 1;
667           else          result.waste += 1;
668         }
669       } else {
670         result.waste += 1;
671       }
672     }
673   }
674   append(me.event_log,
675          [SimTime.current_time(), "water_drop", lat, lon, radius]);
676   return result;
677 }
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);
693       if (cell == nil) {
694         cell = FireCell.new(x + dx, y + dy);
695         me.set_cell(x + dx, y + dy,
696                     cell);
697       }
698       if (cell != nil) {
699         var res = cell.extinguish("retardant");
700         if (res > 0) {
701           result.extinguished += 1;
702         } else {
703           if (res == 0) result.protected += 1;
704           else          result.waste += 1;
705         }
706       } else {
707         result.waste += 1;
708       }
709     }
710   }
711   append(me.event_log,
712          [SimTime.current_time(), "retardant_drop", lat, lon, radius]);
713   return result;
714 }
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);
731       if (cell == nil) {
732         cell = FireCell.new(x + dx, y + dy);
733         me.set_cell(x + dx, y + dy,
734                     cell);
735       }
736       if (cell != nil) {
737         var res = cell.extinguish("foam");
738         if (res > 0) {
739           result.extinguished += 1;
740         } else {
741           if (res == 0) result.protected += 1;
742           else          result.waste += 1;
743         }
744       } else {
745         result.waste += 1;
746       }
747     }
748   }
749   append(me.event_log,
750          [SimTime.current_time(), "foam_drop", lat, lon, radius]);
751   return result;
752 }
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);
759
760   gui.popupTip("Wildfire: Saving state to " ~ filename);
761
762   var i = 0;
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]);
776
777 #    debug.dump(e);
778     i += 1;
779   }
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");
784
785   fgcommand("savexml", args);
786 }
787 ############################################################
788 # Load an event log.
789 #   skip_ahead_until - skip from last event to this time : double (epoch)
790 #                      fast forward from skip_ahead_until
791 #                      to current time.
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) {
795   me.load_count += 1;
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 ~ "'");
801     return;
802   }
803
804   # Fast forward the automaton from the first logged event to the current time.
805   CAFireModels.set_models_enabled(0);
806   var first = 1;
807   var events = props.globals.getNode(logbase).getChildren("event");
808   foreach (var event; events) {
809     if (first) {
810       first = 0;
811       me.reset(1, event.getNode("time-sec").getValue());
812     }
813 #    print("[" ~
814 #          event.getNode("time-sec").getValue() ~ "," ~
815 #          event.getNode("type").getValue() ~ "]");
816     var e = [event.getNode("time-sec").getValue(),
817              event.getNode("type").getValue()];
818
819     # Fast forward state.
820     while (me.generation * me.GENERATION_DURATION < e[0]) {
821 #      print("between event ff " ~ me.generation);
822       me.update();
823     }
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];
829     }
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];
835     }
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];
841     }
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];
847     }
848   }
849   if (first) {
850     me.reset(1, SimTime.current_time());
851     return;
852   }
853
854   var now = SimTime.current_time();
855   if (skip_ahead_until == -1) {
856     me.generation = int(now/me.GENERATION_DURATION);
857   } else {
858     if (me.generation < int(skip_ahead_until/me.GENERATION_DURATION)) {
859       me.generation = int(skip_ahead_until/me.GENERATION_DURATION);
860     }
861     # Catch up with current time. NOTE: This can be very time consuming!
862     while (me.generation * me.GENERATION_DURATION < now)
863       me.update();
864   }
865   CAFireModels.set_models_enabled(getprop(models_enabled_pp));
866 }
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];
872 }
873 ############################################################
874 CAFire.set_cell = func (x, y, cell) {
875   if (me.grid[x] == nil) {
876     me.grid[x] = {};
877   }
878   me.grid[x][y] = cell;
879 }
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.
885     me.generation += 1;
886     me.pass = 0;
887     me.remaining_work = me.active;
888     me.active = [];
889     me.pass_work = size(me.remaining_work)/ me.PASSES + 1;
890     if (me.old == 1) {
891       me.old  = 0;
892       me.next = 1;
893     } else {
894       me.old  = 1;
895       me.next = 0;
896     }
897     if (me.cells_burning > 0) {
898       printlog("info",
899                "Wildfire: generation " ~ me.generation ~ " updating " ~
900                size(me.remaining_work) ~" / " ~ me.cells_created ~
901                " created cells. " ~ me.cells_burning ~ " burning cells.");
902     }
903     # Set LOD.
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);
907     }
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);
911     }
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);
915     }
916     me.cells_burning = 0;
917   }
918
919   me.pass += 1;
920
921   var work = me.pass_work;
922   var c = pop(me.remaining_work);
923   while (c != nil) {
924     if (c.update() != 0) {
925       append(me.active, c);
926       me.cells_burning += c.burning[me.next];
927     }
928     work -= 1;
929     if (work <= 0) return;
930     c = pop(me.remaining_work);
931   }
932 }
933 ############################################################
934 CAFire._loop_ = func(id) {
935   id == me.loopid or return;
936   me.update();
937   settimer(func { me._loop_(id); },
938            me.GENERATION_DURATION * (me.generation + 1/me.PASSES) -
939            SimTime.current_time());
940 }
941 ###############################################################################
942
943 ###############################################################################
944 # Main initialization.
945 var Binary = nil;
946
947 _setlistener("/sim/signals/nasal-dir-initialized", func {
948
949   Binary = mp_broadcast.Binary;
950
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());
956   });
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");
966
967   props.globals.initNode(fire_LOD_pp, 10, "INT");
968   props.globals.initNode(smoke_LOD_pp, 10, "INT");
969
970   SimTime.init();
971   broadcast =
972     mp_broadcast.BroadcastChannel.new(msg_channel_mpp, parse_msg);
973   CAFire.init();
974
975   # Start the score reporting.
976   settimer(score_report_loop, CAFire.GENERATION_DURATION);
977
978   setlistener("/sim/signals/exit", func {
979     if (getprop(report_score_pp) and (CAFire.cells_created > 0))
980       print_score();
981     if (getprop(save_on_exit_pp))
982       CAFire.save_event_log(SAVEDIR ~ "fire_log.xml");
983   });
984
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()) {
988     printlog("alert",
989              "wildfire.nas: Ignored time hack " ~
990              (SimTime.current_time() - time_hack) ~
991              " seconds into the future.");
992     # Skip ahead to current time instead.
993     time_hack = -1;
994   } elsif (time_hack > 0) {
995     printlog("alert",
996              "wildfire.nas: Time hack " ~
997              (SimTime.current_time() - time_hack) ~
998              " seconds ago.");
999   } else {
1000     # Skip ahead to current time instead.
1001     time_hack = -1;
1002   }
1003
1004   if (getprop(event_file_pp) != "") {
1005     settimer(func {
1006       # Delay loading the log until the terrain is there. Note: hack.
1007       CAFire.load_event_log(getprop(event_file_pp), time_hack);
1008     }, 3);      
1009   } elsif (getprop(restore_on_startup_pp)) {
1010     settimer(func {
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);
1014     }, 3);
1015   }
1016
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());
1021   });
1022
1023   # Detect impact.
1024   var impact_node = props.globals.getNode("sim/ai/aircraft/impact/bomb", 1);
1025   setlistener("sim/ai/aircraft/impact/bomb", func(n) {
1026
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);
1034     }
1035
1036   });
1037
1038   printlog("info", "Wildfire ... initialized.");
1039 });
1040 ###############################################################################
1041
1042 ###############################################################################
1043 # Utility functions
1044
1045 # Convert a time string in the format yyyy[:mm[:dd[:hh[:mm[:ss]]]]]
1046 # to seconds since 1970:01:01:00:00:00.
1047 #
1048 # Note: This is an over simplified approximation.
1049 var time_string_to_epoch = func (time) {
1050   var res = [];
1051   if      (string.scanf(time, "%d:%d:%d:%d:%d:%d", var res1 = []) != 0) {
1052     res = res1;
1053   } elsif (string.scanf(time, "%d:%d:%d:%d:%d", var res2 = []) != 0) {
1054     res = 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];
1063   } else {
1064     return -1;
1065   }
1066   return
1067     (res[0] - 1970) * 3.15569e7 +
1068     (res[1] - 1) * 2.63e+6 +
1069     (res[2] - 1) * 86400 +
1070     res[3] * 3600 +
1071     res[4] * 60 +
1072     res[5];
1073 }
1074
1075
1076 ###############################################################################
1077 ## WildFire configuration dialog.
1078 ## Partly based on Till Bush's multiplayer dialog
1079
1080 var CONFIG_DLG = 0;
1081
1082 var dialog = {
1083 #################################################################
1084     init : func (x = nil, y = nil) {
1085         me.x = x;
1086         me.y = y;
1087         me.bg = [0, 0, 0, 0.3];    # background color
1088         me.fg = [[1.0, 1.0, 1.0, 1.0]];
1089         #
1090         # "private"
1091         me.title = "Wildfire";
1092         me.basenode = props.globals.getNode("/environment/wildfire");
1093         me.dialog = nil;
1094         me.namenode = props.Node.new({"dialog-name" : me.title });
1095         me.listeners = [];
1096     },
1097 #################################################################
1098     create : func {
1099         if (me.dialog != nil)
1100             me.close();
1101
1102         me.dialog = gui.Widget.new();
1103         me.dialog.set("name", me.title);
1104         if (me.x != nil)
1105             me.dialog.set("x", me.x);
1106         if (me.y != nil)
1107             me.dialog.set("y", me.y);
1108
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");
1124
1125         var content = me.dialog.addChild("group");
1126         content.set("layout", "vbox");
1127         content.set("halign", "center");
1128         content.set("default-padding", 5);
1129
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],
1139                               "halign"   : "left",
1140                               "property" : b[1]});
1141             w.setBinding("nasal",
1142                          "setprop(\"" ~ b[1] ~ "\"," ~
1143                          "!getprop(\"" ~ b[1] ~ "\"))");
1144         }
1145         me.dialog.addChild("hrule");
1146
1147         # Buttons
1148         var buttons = me.dialog.addChild("group");
1149         buttons.node.setValues({"layout"  : "hbox"});
1150
1151         # Load button.
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()");
1157
1158         # Close button
1159         var close = buttons.addChild("button");
1160         close.node.setValues({"legend"    : "Close",
1161                              "default"   : "true",
1162                              "key"       : "Esc"});
1163         close.setBinding("nasal", "wildfire.dialog.destroy();");
1164         close.setBinding("dialog-close");
1165
1166         fgcommand("dialog-new", me.dialog.prop());
1167         fgcommand("dialog-show", me.namenode);
1168     },
1169 #################################################################
1170     close : func {
1171         fgcommand("dialog-close", me.namenode);
1172     },
1173 #################################################################
1174     destroy : func {
1175         CONFIG_DLG = 0;
1176         me.close();
1177         foreach(var l; me.listeners)
1178             removelistener(l);
1179         delete(gui.dialog, "\"" ~ me.title ~ "\"");
1180     },
1181 #################################################################
1182     show : func {
1183         if (!CONFIG_DLG) {
1184             CONFIG_DLG = 1;
1185             me.init();
1186             me.create();
1187         }
1188     },
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
1198         selector.open();
1199     }
1200 }
1201 ###############################################################################