fix: more consistent trigger naming
[Tsunagari.git] / src / area-tmx.cpp
blob9430175f4ccf1ea7a611e1c6ab06d76feeb5d23a
1 /*********************************
2 ** Tsunagari Tile Engine **
3 ** area-tmx.cpp **
4 ** Copyright 2011-2012 OmegaSDG **
5 *********************************/
7 #include <math.h>
8 #include <vector>
10 #include <boost/foreach.hpp>
11 #include <boost/shared_ptr.hpp>
12 #include <Gosu/Graphics.hpp>
13 #include <Gosu/Image.hpp>
14 #include <Gosu/Math.hpp>
15 #include <Gosu/Timing.hpp>
17 #include "area-tmx.h"
18 #include "entity.h"
19 #include "log.h"
20 #include "python.h"
21 #include "resourcer.h"
22 #include "string.h"
23 #include "tile.h"
24 #include "window.h"
25 #include "world.h"
27 #define ASSERT(x) if (!(x)) return false
29 /* NOTE: In the TMX map format used by Tiled, tileset tiles start counting
30 their Y-positions from 0, while layer tiles start counting from 1. I
31 can't imagine why the author did this, but we have to take it into
32 account.
35 AreaTMX::AreaTMX(Viewport* view,
36 Player* player,
37 Music* music,
38 const std::string& descriptor)
39 : Area(view, player, music, descriptor)
41 // Add TileType #0. Not used, but Tiled's gids start from 1.
42 gids.push_back(NULL);
45 AreaTMX::~AreaTMX()
49 bool AreaTMX::init()
51 return processDescriptor();
55 void AreaTMX::allocateMapLayer()
57 map.push_back(grid_t(dim.y, row_t(dim.x)));
58 grid_t& grid = map[dim.z];
59 for (int y = 0; y < dim.y; y++) {
60 row_t& row = grid[y];
61 for (int x = 0; x < dim.x; x++) {
62 Tile& tile = row[x];
63 new (&tile) Tile(this, x, y, dim.z);
66 dim.z++;
69 bool AreaTMX::processDescriptor()
71 XMLRef doc;
72 XMLNode root;
74 Resourcer* rc = Resourcer::instance();
75 ASSERT(doc = rc->getXMLDoc(descriptor, "area.dtd"));
76 ASSERT(root = doc->root()); // <map>
78 ASSERT(root.intAttr("width", &dim.x));
79 ASSERT(root.intAttr("height", &dim.y));
80 dim.z = 0;
82 for (XMLNode child = root.childrenNode(); child; child = child.next()) {
83 if (child.is("properties")) {
84 ASSERT(processMapProperties(child));
86 else if (child.is("tileset")) {
87 ASSERT(processTileSet(child));
89 else if (child.is("layer")) {
90 ASSERT(processLayer(child));
92 else if (child.is("objectgroup")) {
93 ASSERT(processObjectGroup(child));
97 return true;
100 bool AreaTMX::processMapProperties(XMLNode node)
104 <properties>
105 <property name="author" value="Random J. Hacker"/>
106 <property name="name" value="Wooded AreaTMX"/>
107 <property name="intro_music" value="arrive.ogg"/>
108 <property name="main_music" value="wind.ogg"/>
109 <property name="on_load" value="wood_setup.py"/>
110 <property name="on_focus" value="wood_focus.py"/>
111 <property name="on_update" value="wood_update.py"/>
112 <property name="loop" value="xy"/>
113 <property name="color_overlay" value="255,255,255,127"/>
114 </properties>
116 Resourcer* rc = Resourcer::instance();
118 for (XMLNode child = node.childrenNode(); child; child = child.next()) {
119 std::string name = child.attr("name");
120 std::string value = child.attr("value");
121 if (value.empty())
122 continue;
123 if (name == "author")
124 author = value;
125 else if (name == "name")
126 this->name = value;
127 else if (name == "intro_music") {
128 musicIntro = value;
130 else if (name == "main_music") {
131 musicLoop = value;
133 else if (name == "on_load") {
134 std::string filename = value;
135 if (rc->resourceExists(filename)) {
136 onLoadScripts.push_back(filename);
138 else {
139 Log::err(descriptor,
140 std::string("script not found: ") + filename);
143 else if (name == "on_focus") {
144 std::string filename = value;
145 if (rc->resourceExists(filename)) {
146 onFocusScripts.push_back(filename);
148 else {
149 Log::err(descriptor,
150 std::string("script not found: ") + filename);
153 else if (name == "on_update") {
154 std::string filename = value;
155 if (rc->resourceExists(filename)) {
156 onUpdateScripts.push_back(filename);
158 else {
159 Log::err(descriptor,
160 std::string("script not found: ") + filename);
163 else if (name == "loop") {
164 loopX = value.find('x') != std::string::npos;
165 loopY = value.find('y') != std::string::npos;
167 else if (name == "color_overlay") {
168 Gosu::Color::Channel r, g, b, a;
169 ASSERT(parseRGBA(value, &r, &g, &b, &a));
170 colorOverlay = Gosu::Color(a, r, g, b);
174 return true;
177 bool AreaTMX::processTileSet(XMLNode node)
181 <tileset firstgid="1" name="tiles.sheet" tilewidth="64" tileheight="64">
182 <image source="tiles.sheet" width="256" height="256"/>
183 <tile id="14">
185 </tile>
186 </tileset>
189 TileSet* set;
190 TiledImage img;
191 int tilex, tiley;
192 int firstGid;
194 ASSERT(node.intAttr("tilewidth", &tilex));
195 ASSERT(node.intAttr("tileheight", &tiley));
196 ASSERT(node.intAttr("firstgid", &firstGid));
198 if (tileDim && tileDim != ivec2(tilex, tiley)) {
199 Log::err(descriptor,
200 "<tileset>'s width/height contradict earlier <layer>");
201 return false;
203 tileDim = ivec2(tilex, tiley);
205 for (XMLNode child = node.childrenNode(); child; child = child.next()) {
206 if (child.is("image")) {
207 int pixelw, pixelh;
208 ASSERT(child.intAttr("width", &pixelw) &&
209 child.intAttr("height", &pixelh));
210 int width = pixelw / tileDim.x;
211 int height = pixelh / tileDim.y;
213 std::string source = child.attr("source");
214 tileSets[source] = TileSet(width, height);
215 set = &tileSets[source];
217 // Load tileset image.
218 Resourcer* rc = Resourcer::instance();
219 bool found = rc->getTiledImage(img, source,
220 (unsigned)tilex, (unsigned)tiley, true);
221 if (!found) {
222 Log::err(descriptor, "tileset image not found");
223 return false;
226 // Initialize "vanilla" tile type array.
227 BOOST_FOREACH(ImageRef& tileImg, img) {
228 TileType* type = new TileType(tileImg);
229 set->add(type);
230 gids.push_back(type);
233 else if (child.is("tile")) {
234 // Handle an explicitly declared "non-vanilla" type.
236 if (img.empty()) {
237 Log::err(descriptor,
238 "Tile type processed before tileset image loaded");
239 return false;
242 // "id" is 0-based index of a tile in the current
243 // tileset, if the tileset were a flat array.
244 int id;
245 ASSERT(child.intAttr("id", &id));
247 if (id < 0 || (int)img.size() <= id) {
248 Log::err(descriptor, "tile type id is invalid");
249 return false;
252 // Initialize a default TileType, we'll build on that.
253 TileType* type = new TileType(img[id]);
254 ASSERT(processTileType(child, *type, img, id));
256 // "gid" is the global area-wide id of the tile.
257 size_t gid = id + firstGid;
258 delete gids[gid]; // "vanilla" type
259 gids[gid] = type;
260 set->set(id, type);
264 return true;
267 bool AreaTMX::processTileType(XMLNode node, TileType& type, TiledImage& img, int id)
271 <tile id="8">
272 <properties>
273 <property name="flags" value="nowalk"/>
274 <property name="onEnter" value="skid();speed(2)"/>
275 <property name="onLeave" value="undo()"/>
276 <property name="onUse" value="undo()"/>
277 </properties>
278 </tile>
279 <tile id="14">
280 <properties>
281 <property name="members" value="1,2,3,4"/>
282 <property name="speed" value="2"/>
283 </properties>
284 </tile>
287 // The id has already been handled by processTileSet, so we don't have
288 // to worry about it.
290 Resourcer* rc = Resourcer::instance();
292 XMLNode child = node.childrenNode(); // <properties>
293 for (child = child.childrenNode(); child; child = child.next()) {
294 // Each <property>...
295 std::string name = child.attr("name");
296 std::string value = child.attr("value");
297 if (name == "flags") {
298 ASSERT(splitTileFlags(value, &type.flags));
300 else if (name == "on_enter") {
301 std::string filename = value;
302 if (!rc->resourceExists(filename)) {
303 Log::err(descriptor,
304 "script not found: " + filename);
305 continue;
307 type.onEnter.push_back(filename);
309 else if (name == "on_leave") {
310 std::string filename = value;
311 if (!rc->resourceExists(filename)) {
312 Log::err(descriptor,
313 "script not found: " + filename);
314 continue;
316 type.onLeave.push_back(filename);
318 else if (name == "on_use") {
319 std::string filename = value;
320 if (!rc->resourceExists(filename)) {
321 Log::err(descriptor,
322 "script not found: " + filename);
323 continue;
325 type.onUse.push_back(filename);
327 else if (name == "members") {
328 std::string memtemp;
329 std::vector<std::string> members;
330 std::vector<std::string>::iterator it;
331 memtemp = value;
332 members = splitStr(memtemp, ",");
334 // Make sure the first member is this tile.
335 if (atoi(members[0].c_str()) != id) {
336 Log::err(descriptor, "first member of tile"
337 " id " + itostr(id) +
338 " animation must be itself.");
339 return false;
342 // Add frames to our animation.
343 // We already have one from TileType's constructor.
344 for (it = members.begin()+1; it < members.end(); it++) {
345 int idx = atoi(it->c_str());
346 if (idx < 0 || (int)img.size() <= idx) {
347 Log::err(descriptor, "frame index out "
348 "of range for animated tile");
349 return false;
351 type.anim.addFrame(img[idx]);
354 else if (name == "speed") {
355 double hertz;
356 ASSERT(child.doubleAttr("value", &hertz));
357 int len = (int)(1000.0/hertz);
358 type.anim.setFrameLen(len);
362 return true;
365 bool AreaTMX::processLayer(XMLNode node)
369 <layer name="Tiles0" width="5" height="5">
370 <properties>
372 </properties>
373 <data>
374 <tile gid="9"/>
375 <tile gid="9"/>
376 <tile gid="9"/>
378 <tile gid="3"/>
379 <tile gid="9"/>
380 <tile gid="9"/>
381 </data>
382 </layer>
385 int x, y;
386 double depth;
387 ASSERT(node.intAttr("width", &x));
388 ASSERT(node.intAttr("height", &y));
390 if (dim.x != x || dim.y != y) {
391 Log::err(descriptor, "layer x,y size != map x,y size");
392 return false;
395 allocateMapLayer();
397 for (XMLNode child = node.childrenNode(); child; child = child.next()) {
398 if (child.is("properties")) {
399 ASSERT(processLayerProperties(child, &depth));
401 else if (child.is("data")) {
402 ASSERT(processLayerData(child, dim.z - 1));
406 return true;
409 bool AreaTMX::processLayerProperties(XMLNode node, double* depth)
413 <properties>
414 <property name="layer" value="0"/>
415 </properties>
418 bool layerFound = false;
420 for (XMLNode child = node.childrenNode(); child; child = child.next()) {
421 std::string name = child.attr("name");
422 std::string value = child.attr("value");
423 if (name == "layer") {
424 layerFound = true;
425 ASSERT(child.doubleAttr("value", depth));
426 if (depth2idx.find(*depth) != depth2idx.end()) {
427 Log::err(descriptor,
428 "depth used multiple times");
429 return false;
432 depth2idx[*depth] = dim.z - 1;
433 idx2depth.push_back(*depth);
434 // Effectively idx2depth[dim.z - 1] = depth;
438 if (!layerFound)
439 Log::err(descriptor, "<layer> must have layer property");
440 return layerFound;
443 bool AreaTMX::processLayerData(XMLNode node, int z)
447 <data>
448 <tile gid="9"/>
449 <tile gid="9"/>
450 <tile gid="9"/>
452 <tile gid="3"/>
453 <tile gid="9"/>
454 <tile gid="9"/>
455 </data>
458 int x = 0, y = 0;
460 for (XMLNode child = node.childrenNode(); child; child = child.next()) {
461 if (child.is("tile")) {
462 int gid;
463 ASSERT(child.intAttr("gid", &gid));
465 if (gid < 0 || (int)gids.size() <= gid) {
466 Log::err(descriptor, "invalid tile gid");
467 return false;
470 // A gid of zero means there is no tile at this
471 // position on this layer.
472 if (gid > 0) {
473 TileType* type = gids[gid];
474 Tile& tile = map[z][y][x];
475 type->allOfType.push_back(&tile);
476 tile.parent = type;
479 if (++x == dim.x) {
480 x = 0;
481 y++;
486 return true;
489 bool AreaTMX::processObjectGroup(XMLNode node)
493 <objectgroup name="Prop0" width="5" height="5">
494 <properties>
495 <property name="layer" value="0.0"/>
496 </properties>
497 <object name="tile2" gid="7" x="64" y="320">
498 <properties>
499 <property name="onEnter" value="speed(0.5)"/>
500 <property name="onLeave" value="undo()"/>
501 <property name="onUse" value="undo()"/>
502 <property name="exit" value="grassfield.area,1,1,0"/>
503 <property name="flags" value="npc_nowalk"/>
504 </properties>
505 </object>
506 </objectgroup>
509 double invalid = (double)NAN; // Not a number.
510 int x, y;
511 ASSERT(node.intAttr("width", &x));
512 ASSERT(node.intAttr("height", &y));
514 double depth = invalid;
516 if (dim.x != x || dim.y != y) {
517 Log::err(descriptor, "objectgroup x,y size != map x,y size");
518 return false;
521 for (XMLNode child = node.childrenNode(); child; child = child.next()) {
522 if (child.is("properties")) {
523 ASSERT(processObjectGroupProperties(child, &depth));
525 else if (child.is("object")) {
526 ASSERT(depth != invalid);
527 int z = depth2idx[depth];
528 ASSERT(processObject(child, z));
532 return true;
535 bool AreaTMX::processObjectGroupProperties(XMLNode node, double* depth)
539 <properties>
540 <property name="layer" value="0.0"/>
541 </properties>
543 bool layerFound = false;
545 for (XMLNode child = node.childrenNode(); child; child = child.next()) {
546 std::string name = child.attr("name");
547 std::string value = child.attr("value");
548 if (name == "layer") {
549 layerFound = true;
550 ASSERT(child.doubleAttr("value", depth));
551 if (depth2idx.find(*depth) == depth2idx.end()) {
552 allocateMapLayer();
553 depth2idx[*depth] = dim.z - 1;
554 idx2depth.push_back(*depth);
555 // Effectively idx2depth[dim.z - 1] = depth;
560 if (!layerFound)
561 Log::err(descriptor, "<objectgroup> must have layer property");
562 return layerFound;
565 bool AreaTMX::processObject(XMLNode node, int z)
569 <object name="tile2" gid="7" x="64" y="320">
570 <properties>
571 <property name="onEnter" value="speed(0.5)"/>
572 <property name="onLeave" value="undo()"/>
573 <property name="onUse" value="undo()"/>
574 <property name="exit" value="grassfield.area,1,1,0"/>
575 <property name="flags" value="npc_nowalk"/>
576 </properties>
577 </object>
578 <object name="foo" x="0" y="0" width="64" height="64">
580 </object>
583 Resourcer* rc = Resourcer::instance();
585 // Gather object properties now. Assign them to tiles later.
586 bool wwide[5], hwide[5]; /* wide exit in dimensions: width, height */
588 std::vector<std::string> onEnter, onLeave, onUse;
589 boost::scoped_ptr<Exit> exit[5];
590 boost::optional<double> layermods[5];
591 unsigned flags = 0x0;
593 XMLNode child = node.childrenNode(); // <properties>
594 for (child = child.childrenNode(); child; child = child.next()) {
595 // Each <property>...
596 std::string name = child.attr("name");
597 std::string value = child.attr("value");
598 if (name == "flags") {
599 ASSERT(splitTileFlags(value, &flags));
601 else if (name == "on_enter") {
602 std::string filename = value;
603 if (!rc->resourceExists(filename)) {
604 Log::err(descriptor,
605 "script not found: " + filename);
606 continue;
608 onEnter.push_back(filename);
610 else if (name == "on_leave") {
611 std::string filename = value;
612 if (!rc->resourceExists(filename)) {
613 Log::err(descriptor,
614 "script not found: " + filename);
615 continue;
617 onLeave.push_back(filename);
619 else if (name == "on_use") {
620 std::string filename = value;
621 if (!rc->resourceExists(filename)) {
622 Log::err(descriptor,
623 "script not found: " + filename);
624 continue;
626 onUse.push_back(filename);
628 else if (name == "exit") {
629 exit[EXIT_NORMAL].reset(new Exit);
630 ASSERT(parseExit(value, exit[EXIT_NORMAL].get(), &wwide[EXIT_NORMAL], &hwide[EXIT_NORMAL]));
631 flags |= TILE_NOWALK_NPC;
633 else if (name == "exit:up") {
634 exit[EXIT_UP].reset(new Exit);
635 ASSERT(parseExit(value, exit[EXIT_UP].get(), &wwide[EXIT_UP], &hwide[EXIT_UP]));
637 else if (name == "exit:down") {
638 exit[EXIT_DOWN].reset(new Exit);
639 ASSERT(parseExit(value, exit[EXIT_DOWN].get(), &wwide[EXIT_DOWN], &hwide[EXIT_DOWN]));
641 else if (name == "exit:left") {
642 exit[EXIT_LEFT].reset(new Exit);
643 ASSERT(parseExit(value, exit[EXIT_LEFT].get(), &wwide[EXIT_LEFT], &hwide[EXIT_LEFT]));
645 else if (name == "exit:right") {
646 exit[EXIT_RIGHT].reset(new Exit);
647 ASSERT(parseExit(value, exit[EXIT_RIGHT].get(), &wwide[EXIT_RIGHT], &hwide[EXIT_RIGHT]));
649 else if (name == "layermod") {
650 double mod;
651 ASSERT(child.doubleAttr("value", &mod));
652 layermods[EXIT_NORMAL].reset(mod);
653 flags |= TILE_NOWALK_NPC;
655 else if (name == "layermod:up") {
656 double mod;
657 ASSERT(child.doubleAttr("value", &mod));
658 layermods[EXIT_UP].reset(mod);
660 else if (name == "layermod:down") {
661 double mod;
662 ASSERT(child.doubleAttr("value", &mod));
663 layermods[EXIT_DOWN].reset(mod);
665 else if (name == "layermod:left") {
666 double mod;
667 ASSERT(child.doubleAttr("value", &mod));
668 layermods[EXIT_LEFT].reset(mod);
670 else if (name == "layermod:right") {
671 double mod;
672 ASSERT(child.doubleAttr("value", &mod));
673 layermods[EXIT_RIGHT].reset(mod);
677 // Apply these properties directly to one or more tiles in a rectangle
678 // of the map. We don't keep an intermediary "object" object lying
679 // around.
680 int x, y, w, h;
681 ASSERT(node.intAttr("x", &x));
682 ASSERT(node.intAttr("y", &y));
683 x /= tileDim.x;
684 y /= tileDim.y;
686 if (node.hasAttr("gid")) {
687 // This is one of Tiled's "Tile Objects". It is one tile wide
688 // and high.
689 y = y - 1; // Bug in tiled. The y is off by one.
690 w = 1;
691 h = 1;
693 // We don't actually use the object gid. It is supposed to
694 // indicate which tile our object is rendered as, but for
695 // Tsunagari, tile objects are always transparent and reveal
696 // the tile below.
698 else {
699 // This is one of Tiled's "Objects". It has a width and height.
700 ASSERT(node.intAttr("width", &w));
701 ASSERT(node.intAttr("height", &h));
702 w /= tileDim.x;
703 h /= tileDim.y;
706 // We know which Tiles are being talked about now... yay
707 for (int Y = y; Y < y + h; Y++) {
708 for (int X = x; X < x + w; X++) {
709 Tile& tile = map[z][Y][X];
711 tile.flags |= flags;
712 for (int i = 0; i < 5; i++) {
713 if (exit[i]) {
714 tile.exits[i] = new Exit(*exit[i].get());
715 int dx = X - x;
716 int dy = Y - y;
717 if (wwide[i])
718 tile.exits[i]->coords.x += dx;
719 if (hwide[i])
720 tile.exits[i]->coords.y += dy;
723 for (int i = 0; i < 5; i++)
724 if (layermods[i])
725 tile.layermods[i] = layermods[i];
726 tile.onEnter = onEnter;
727 tile.onLeave = onLeave;
728 tile.onUse = onUse;
732 return true;
735 bool AreaTMX::splitTileFlags(const std::string& strOfFlags, unsigned* flags)
737 std::vector<std::string> strs = splitStr(strOfFlags, ",");
739 BOOST_FOREACH(const std::string& str, strs) {
740 if (str == "nowalk")
741 *flags |= TILE_NOWALK;
742 else {
743 Log::err(descriptor, "invalid tile flag: " + str);
744 return false;
747 return true;
750 // FIXME: "1 2", " ", and "" are considered valid, " -3" not valid
751 bool isIntegerOrPlus(const std::string& s)
753 for (unsigned i = 0; i < s.size(); i++) {
754 char c = s[i];
755 if (isdigit(c) || isspace(c) || c == '+' ||
756 (c == '-' && i == 0))
757 continue;
758 return false;
760 return true;
763 bool AreaTMX::parseExit(const std::string& dest, Exit* exit,
764 bool* wwide, bool* hwide)
768 Format: destination area, x, y, z
769 E.g.: "babysfirst.area,1,3,0"
772 std::vector<std::string> strs = splitStr(dest, ",");
774 if (strs.size() != 4) {
775 Log::err(descriptor, "<exit />: invalid format");
776 return false;
779 std::string area = strs[0],
780 xstr = strs[1],
781 ystr = strs[2],
782 zstr = strs[3];
784 if (!isIntegerOrPlus(xstr) ||
785 !isIntegerOrPlus(ystr) ||
786 !isIntegerOrPlus(zstr)) {
787 Log::err(descriptor, "<exit />: invalid format");
788 return false;
791 exit->area = area;
792 exit->coords.x = atoi(xstr.c_str());
793 exit->coords.y = atoi(ystr.c_str());
794 exit->coords.z = atof(zstr.c_str());
796 *wwide = xstr.find('+') != std::string::npos;
797 *hwide = ystr.find('+') != std::string::npos;
799 return true;
802 bool AreaTMX::parseRGBA(const std::string& str,
803 Gosu::Color::Channel* r,
804 Gosu::Color::Channel* g,
805 Gosu::Color::Channel* b,
806 Gosu::Color::Channel* a)
808 std::vector<std::string> strs = splitStr(str, ",");
810 if (strs.size() != 4) {
811 Log::err(descriptor, "invalid RGBA format");
812 return false;
815 Gosu::Color::Channel* channels[] = { r, g, b, a };
817 for (int i = 0; i < 4; i++) {
818 std::string s = strs[i];
819 if (!isInteger(s)) {
820 Log::err(descriptor, "invalid RGBA format");
821 return false;
823 int v = atoi(s.c_str());
824 if (!(0 <= v && v < 256)) {
825 Log::err(descriptor,
826 "RGBA values must be between 0 and 255");
827 return false;
829 *channels[i] = (Gosu::Color::Channel)v;
832 return true;