console logger; template loader
[dd2d.git] / d2dmon.d
blob0fd83644361d2acd48a5e3b3b418e91d9aee5622
1 module d2dmon is aliced;
3 import console;
4 import tparser;
5 import wadarc;
8 // ////////////////////////////////////////////////////////////////////////// //
9 // thing type in map
10 enum ThingId {
11 None = 0,
12 Player1,
13 Player2,
14 DMStart,
16 FirstItem = 100,
17 Clip = FirstItem,
18 Shell,
19 Rocket,
20 Cell,
21 Ammo,
22 ShellBox,
23 RocketBox,
24 CellPack,
25 StimPack,
26 MediKit,
27 BackPack,
28 Chainsaw,
29 Shotgun,
30 SuperShotgun,
31 MachineGun,
32 RocketLauncher,
33 Plasmagun,
34 BFG900,
35 Armor1,
36 Armor2,
37 MegaSphere,
38 Invulnerability,
39 Aqualung,
40 RedKey,
41 GreenKey,
42 BlueKey,
43 ProtectionSuit,
44 Super, //???
45 RedTorch,
46 GreenTorch,
47 BlueTorch,
48 Gor1, //???
49 FCan, //???
50 Gun2, //???
51 LastItem = Gun2,
53 FirstMonter = 200,
54 Demon = FirstMonter,
55 Imp,
56 Zombie,
57 Sergeant,
58 Cyberdemon,
59 Chaingunner,
60 BaronOfHell,
61 HellKnight,
62 Cacodemon,
63 LostSoul,
64 PainElemental,
65 SpiderMastermind,
66 Arachnotron,
67 Mancubus,
68 Revenant,
69 Archvile,
70 Fish,
71 Barrel,
72 Robot,
73 Man,
74 LastMonster = Man,
78 // ////////////////////////////////////////////////////////////////////////// //
79 enum MonType {
80 Nobody,
81 Demon,
82 Imp,
83 Zombie,
84 Sergeant,
85 Cyberdemon,
86 Chaingunner,
87 BaronOfHell,
88 HellKnight,
89 Cacodemon,
90 LostSoul,
91 PainElemental,
92 SpiderMastermind,
93 Arachnotron,
94 Mancubus,
95 Revenant,
96 Archvile,
97 Fish,
98 Barrel,
99 Robot,
100 Man,
104 // ////////////////////////////////////////////////////////////////////////// //
105 enum ItemType {
106 Nothing,
107 Clip,
108 Shell,
109 Rocket,
110 Cell,
111 Ammo,
112 ShellBox,
113 RocketBox,
114 CellPack,
115 StimPack,
116 MediKit,
117 BackPack,
118 Chainsaw,
119 Shotgun,
120 SuperShotgun,
121 MachineGun,
122 RocketLauncher,
123 Plasmagun,
124 BFG900,
125 Armor1,
126 Armor2,
127 MegaSphere,
128 Invulnerability,
129 Aqualung,
130 RedKey,
131 GreenKey,
132 BlueKey,
133 ProtectionSuit,
134 Super, //???
135 RedTorch,
136 GreenTorch,
137 BlueTorch,
138 Gor1, //???
139 FCan, //???
140 Gun2, //???
144 // ////////////////////////////////////////////////////////////////////////// //
145 __gshared ItemTemplate[ItemType.max+1] itemTpls;
146 __gshared MonsterTemplate[MonType.max+1] monTpls;
149 // ////////////////////////////////////////////////////////////////////////// //
150 class AnimAction {
151 int len; // in frames
152 this (int alen) { len = (alen < 0 ? 0 : alen); }
153 override string toString () const {
154 import std.string : format;
155 return "<noop>(%s)".format(len);
160 class AnimActionRemove : AnimAction {
161 this () { super(0); }
162 override string toString () const {
163 import std.string : format;
164 return (len != 1 ? "@remove(%s)".format(len) : "@remove");
169 class AnimActionSpr : AnimAction {
170 string spr;
171 this (const(char)[] aspr, int alen) { super(alen); spr = (aspr !is null ? aspr.idup : null); }
172 override string toString () const {
173 import std.string : format;
174 return (len != 1 ? "%s(%s)".format(spr, len) : spr);
179 class AnimActionJump : AnimAction {
180 int dest; // absolute; <0: unresolved forward
181 this (int adest) { super(0); dest = adest; }
182 override string toString () const {
183 import std.string : format;
184 return "@goto(%s) %s".format(len, dest);
189 class AnimActionJumpState : AnimActionJump {
190 string stname;
191 this (int adest, string aname) { super(adest); stname = aname; }
192 override string toString () const {
193 import std.string : format;
194 return "@goto(%s) %s".format(len, dest);
199 // ////////////////////////////////////////////////////////////////////////// //
200 struct ActionScript {
201 private:
202 string lastLabel;
203 int[string] fwdlabels;
204 int xidx = -2;
206 public:
207 AnimAction[] actions;
208 int[string] actlabels;
210 public:
211 void clear () {
212 actions = null;
213 actlabels.clear;
214 lastLabel = null;
215 fwdlabels.clear;
216 xidx = -2;
219 int getLabelId(bool canadd=false) (string name) {
220 if (auto lbi = name in actlabels) return *lbi;
221 if (auto lbi = name in fwdlabels) return *lbi;
222 static if (canadd) {
223 // new forward ref
224 int res = xidx--;
225 fwdlabels[name] = res;
226 return res;
227 } else {
228 return -1;
232 void finish () {
233 // check unresolved labels
234 foreach (auto kv; fwdlabels.byKeyValue) {
235 if (kv.value != -1) throw new Exception("undefined label: '"~kv.key~"'");
237 lastLabel = null;
238 fwdlabels.clear;
239 xidx = -2;
242 string labelAt (int idx) {
243 foreach (auto kv; actlabels.byKeyValue) if (kv.value == idx) return kv.key;
244 return null;
248 private import std.stdio : File;
249 void dump (File fo) {
250 fo.writeln(" states {");
251 foreach (immutable idx, auto act; actions) {
252 foreach (auto kv; actlabels.byKeyValue) if (kv.value == idx) fo.writeln(" ", kv.key, ": /*", idx, "*/");
253 if (auto jp = cast(AnimActionJumpState)act) {
254 fo.write(" @setstate");
255 if (jp.len != 0) fo.write("(", jp.len, ")");
256 fo.write(" ", jp.stname);
257 auto lab = labelAt(jp.dest);
258 if (lab != jp.stname) fo.write(", ", lab);
259 fo.writeln(";");
260 } else if (auto jp = cast(AnimActionJump)act) {
261 fo.write(" @goto");
262 if (jp.len != 0) fo.write("(", jp.len, ")");
263 fo.writeln(" ", labelAt(jp.dest), ";");
264 } else {
265 fo.writeln(" ", act.toString, ";");
268 fo.writeln(" }");
273 // ////////////////////////////////////////////////////////////////////////// //
274 void parseCommand(PR) (ref ActionScript asc, ref PR parser, bool allowFrameCount) {
275 int parseFrameCount () {
276 if (!allowFrameCount) return 0;
277 if (parser.tk == parser.TType.Delim && parser.tchar == '(') {
278 parser.popFront();
279 parser.expectNum();
280 if (parser.tnum < 0) parser.error("positive number expected");
281 auto res = parser.tnum;
282 parser.popFront();
283 parser.skipDelim(')');
284 return res;
286 return 0;
289 switch (parser.tstr) {
290 case "@remove":
291 parser.popFront();
292 asc.actions ~= new AnimActionRemove();
293 parseFrameCount();
294 break;
295 case "@loop":
296 if (asc.lastLabel.length == 0) parser.error("there is no label to loop");
297 parser.popFront();
298 asc.actions ~= new AnimActionJump(asc.getLabelId!true(asc.lastLabel));
299 asc.actions[$-1].len = parseFrameCount();
300 break;
301 case "@goto":
302 parser.popFront();
303 parser.expectStr();
304 asc.actions ~= new AnimActionJump(asc.getLabelId!true(parser.tstr.idup));
305 parser.popFront();
306 asc.actions[$-1].len = parseFrameCount();
307 break;
308 case "@setstate": // state[label]
309 parser.popFront();
310 parser.expectStr();
311 auto st = parser.tstr.idup;
312 auto lb = st;
313 parser.popFront();
314 if (parser.tk == parser.TType.Delim && parser.tchar == '[') {
315 parser.popFront();
316 parser.expectStr();
317 lb = parser.tstr.idup;
318 parser.popFront();
319 parser.skipDelim(']');
321 asc.actions ~= new AnimActionJumpState(asc.getLabelId!true(lb), st);
322 asc.actions[$-1].len = parseFrameCount();
323 break;
324 case "@sound":
325 parser.popFront();
326 parseFrameCount();
327 parser.skipDelim('(');
328 parser.expectStr();
329 parser.popFront();
330 parser.skipDelim(')');
331 break;
332 default: parser.error("unknown command: '"~parser.tstr.idup~"'");
337 // open "{" skipped, eats closing "}"
338 void parseStates(PR) (ref ActionScript asc, ref PR parser) {
339 mainloop: for (;;) {
340 if (parser.tk == parser.TType.Delim) {
341 if (parser.tchar == '}') { parser.popFront(); break; }
342 if (parser.tchar == ';') { parser.popFront(); continue mainloop; }
343 parser.error("unexpected token");
345 parser.expectStr();
346 // label
347 if (parser.isLabel) {
348 asc.lastLabel = parser.tstr[0..$-1].idup;
349 if (parser.isCommand) parser.error("invalid label name: '"~asc.lastLabel~"'");
350 if (asc.lastLabel in asc.actlabels) parser.error("duplicate label: '"~asc.lastLabel~"'");
351 parser.popFront();
352 asc.actlabels[asc.lastLabel] = cast(int)asc.actions.length;
353 if (auto lbidx = asc.lastLabel in asc.fwdlabels) {
354 // fix forward references
355 foreach (AnimAction act; asc.actions) {
356 if (auto jp = cast(AnimActionJump)act) {
357 if (jp.dest == *lbidx) jp.dest = cast(int)asc.actions.length;
360 *lbidx = -1; // mark as processed
362 continue mainloop;
364 // command
365 if (parser.isCommand) {
366 asc.parseCommand(parser, true);
367 parser.skipDelim(';');
368 continue mainloop;
370 // sprite
371 string spr = parser.tstr.idup;
372 parser.popFront();
373 int frcount = 1;
374 if (parser.tk == parser.TType.Delim && parser.tchar == '(') {
375 parser.popFront();
376 parser.expectNum();
377 if (parser.tnum < 0) parser.error("positive number expected");
378 frcount = parser.tnum;
379 parser.popFront();
380 parser.skipDelim(')');
382 asc.actions ~= new AnimActionSpr(spr, frcount);
383 // has commands?
384 if (parser.isCommand) {
385 // sprite action should last 0 frames, last command should last frcount frames
386 while (parser.isCommand) {
387 asc.actions[$-1].len = 0;
388 asc.parseCommand(parser, false);
389 asc.actions[$-1].len = frcount;
392 parser.skipDelim(';');
397 // ////////////////////////////////////////////////////////////////////////// //
398 class ThingTemplate {
399 enum FieldIntPos;
400 enum FieldStr;
402 string classname = null;
403 string name; // thing name
404 @FieldIntPos int radius = -1;
405 @FieldIntPos int height = -1;
406 @FieldIntPos int hitpoints = -1;
407 @FieldStr string base = null;
408 ActionScript asc; // action script
410 // should be at starting "{"
411 this(PR) (ref PR parser) {
412 if (classname is null) classname = "Thing";
413 int getEqIntPos () {
414 parser.popFront(); // skip name
415 parser.skipDelim('=');
416 if (parser.tk != parser.TType.Num) parser.error("number expected");
417 if (parser.tnum < 0) parser.error("positive number expected");
418 int res = parser.tnum;
419 parser.popFront(); // skip number
420 parser.skipDelim(';');
421 return res;
424 string getStr () {
425 parser.popFront(); // skip name
426 parser.skipDelim('=');
427 if (parser.tk != parser.TType.Str) parser.error("string expected");
428 string res = parser.tstr.idup;
429 parser.popFront(); // skip string
430 parser.skipDelim(';');
431 return res;
434 parser.expectStr();
435 name = parser.tstr.idup;
436 parser.popFront();
437 parser.skipDelim('{');
438 mainloop: for (;;) {
439 if (parser.empty) parser.error("unexpected end of file");
440 if (parser.tk == parser.TType.Delim) {
441 if (parser.tchar == '}') { parser.popFront(); break; }
442 if (parser.tchar == ';') { parser.popFront(); continue mainloop; }
443 parser.error("unexpected token");
445 parser.expectStr();
446 if (parser.tstr == "states") {
447 parser.popFront();
448 parser.skipDelim('{');
449 asc.parseStates(parser);
450 continue mainloop;
452 auto fld = findField(parser.tstr);
453 if (!fld.valid) parser.error("unexpected token: '"~parser.tstr.idup~"'");
454 switch (fld.type) {
455 case fld.Type.IntPos: fld = getEqIntPos(); break;
456 case fld.Type.Str: fld = getStr(); break;
457 default: parser.error("invalid type for field '"~fld.name~"'");
460 asc.finish();
461 checkProps();
464 final void err (string msg) { throw new Exception("'"~name~"': "~msg); }
466 void checkProps () {
467 if (radius < 0) err("undefined radius");
468 if (height < 0) err("undefined height");
469 if (hitpoints < 0) hitpoints = 0;
470 if (base is null) err("undefined base path");
473 public:
474 struct FieldInfo {
475 enum Type {
476 None,
477 IntPos, // positive integer
478 Str,
480 Type type;
481 void* ptr;
482 string name;
484 @property bool valid () const pure nothrow @safe @nogc => (ptr !is null);
486 void opAssign (int v) {
487 if (ptr is null) throw new Exception("cannot assign to inexistent field '"~name~"'");
488 switch (type) {
489 case Type.IntPos:
490 if (v < 0) throw new Exception("cannot assign negative number to intpos field '"~name~"'");
491 *(cast(int*)ptr) = v;
492 break;
493 case Type.Str:
494 import std.conv : to;
495 *(cast(string*)ptr) = to!string(v);
496 break;
497 default: throw new Exception("cannot assign number to invalid typed field '"~name~"'");
501 void opAssign(T) (T v) if (is(T == char[]) || is(T == const(char)[]) || is(T == immutable(char)[])) {
502 if (ptr is null) throw new Exception("cannot assign to inexistent field '"~name~"'");
503 switch (type) {
504 case Type.IntPos:
505 import std.conv : to;
506 int n = to!int(v);
507 if (n < 0) throw new Exception("cannot assign negative number to intpos field '"~name~"'");
508 *(cast(int*)ptr) = n;
509 break;
510 case Type.Str:
511 if (v is null) {
512 *(cast(string*)ptr) = null;
513 } else {
514 static if (is(T == immutable(char)[])) {
515 *(cast(string*)ptr) = v;
516 } else {
517 *(cast(string*)ptr) = v.idup;
520 break;
521 default: throw new Exception("cannot assign string to invalid typed field '"~name~"'");
526 mixin(FindFieldMixin);
528 enum FindFieldMixin = q{
529 FieldInfo findField (const(char)[] name) {
530 foreach (string mem; __traits(allMembers, typeof(this))) {
531 static if (is(typeof(mixin("this."~mem)))) {
532 if (mem == name) {
533 import std.traits : hasUDA;
534 static if (hasUDA!(mixin("this."~mem), FieldIntPos)) {
535 return FieldInfo(FieldInfo.Type.IntPos, mixin("cast(void*)&(this."~mem~")"), mem);
536 } else static if (hasUDA!(mixin("this."~mem), FieldStr)) {
537 return FieldInfo(FieldInfo.Type.Str, mixin("cast(void*)&(this."~mem~")"), mem);
542 return FieldInfo(FieldInfo.Type.None, null, "<null>");
546 private import std.stdio : File;
547 enum DumpMixin = q{
548 void dump (File fo) {
549 fo.writeln(classname, " ", name, " {");
550 foreach (string mem; __traits(allMembers, typeof(this))) {
551 static if (is(typeof(mixin("this."~mem)))) {
552 import std.traits : hasUDA;
553 static if (hasUDA!(mixin("this."~mem), FieldIntPos)) {
554 mixin("fo.writeln(` "~mem~" = `, this."~mem~", `;`);");
555 } else static if (hasUDA!(mixin("this."~mem), FieldStr)) {
556 mixin("fo.writeln(` "~mem~" = \"`, this."~mem~", `\";`);");
560 asc.dump(fo);
561 fo.writeln("}");
562 fo.writeln("//=============");
565 mixin(DumpMixin);
569 // ////////////////////////////////////////////////////////////////////////// //
570 final class ItemTemplate : ThingTemplate {
571 mixin("override "~FindFieldMixin);
572 mixin("override "~DumpMixin);
574 override void checkProps () {
575 super.checkProps();
578 // "Item" is skipped
579 this(PR) (ref PR parser) { classname = "Item"; super(parser); }
583 // ////////////////////////////////////////////////////////////////////////// //
584 final class MonsterTemplate : ThingTemplate {
585 @FieldIntPos int painin = -1;
586 @FieldIntPos int xvel = -1;
587 @FieldIntPos int yvel = -1;
588 @FieldIntPos int slophit = -1;
589 @FieldIntPos int painout = -1;
591 mixin("override "~FindFieldMixin);
592 mixin("override "~DumpMixin);
594 override void checkProps () {
595 if (hitpoints < 0) err("undefined hitpoints");
596 if (painin < 0) painin = 0;
597 if (xvel < 0) xvel = 0;
598 if (yvel < 0) yvel = 0;
599 if (slophit < 0) slophit = 0;
600 if (painout < 0) painout = 0;
601 super.checkProps();
604 // "Actor" is skipped
605 this(PR) (ref PR parser) { classname = "Actor"; super(parser); }
609 // ////////////////////////////////////////////////////////////////////////// //
610 public void loadTemplates () {
611 import std.conv : to;
612 conwriteln("loading monsters...");
614 auto parser = TextParser(loadTextFile("monsters.txt"));
615 while (!parser.empty) {
616 if (parser.tstr != "Actor") parser.error("'Actor' expected, got '"~parser.tstr.idup~"'");
617 parser.popFront();
618 parser.expectStr();
619 int midx = -1;
620 try {
621 midx = cast(int)(to!MonType(parser.tstr));
622 } catch (Exception e) {
623 conwriteln("unknown monster: '", parser.tstr, "'");
624 assert(0);
626 if (midx < 1 || midx > MonType.max) {
627 conwriteln("unknown monster: '", parser.tstr, "'");
628 assert(0);
630 monTpls[midx] = new MonsterTemplate(parser);
631 conwriteln(monTpls[midx].name, ": loaded");
632 //mon.dump(stdout);
635 conwriteln("loading items...");
637 auto parser = TextParser(loadTextFile("items.txt"));
638 while (!parser.empty) {
639 if (parser.tstr != "Item") parser.error("'Item' expected, got '"~parser.tstr.idup~"'");
640 parser.popFront();
641 parser.expectStr();
642 int midx = -1;
643 try {
644 midx = cast(int)(to!ItemType(parser.tstr));
645 } catch (Exception e) {
646 conwriteln("unknown item: '", parser.tstr, "'");
647 assert(0);
649 if (midx < 1 || midx > ItemType.max) {
650 conwriteln("unknown item: '", parser.tstr, "'");
651 assert(0);
653 itemTpls[midx] = new ItemTemplate(parser);
654 conwriteln(itemTpls[midx].name, ": loaded");
655 //mon.dump(stdout);