1 /*********************************
2 ** Tsunagari Tile Engine **
4 ** Copyright 2011-2012 OmegaSDG **
5 *********************************/
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>
21 #include "resourcer.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
35 AreaTMX::AreaTMX(Viewport
* view
,
38 const std::string
& descriptor
)
39 : Area(view
, player
, music
, descriptor
)
41 // Add TileType #0. Not used, but Tiled's gids start from 1.
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
++) {
61 for (int x
= 0; x
< dim
.x
; x
++) {
63 new (&tile
) Tile(this, x
, y
, dim
.z
);
69 bool AreaTMX::processDescriptor()
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
));
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
));
100 bool AreaTMX::processMapProperties(XMLNode node
)
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"/>
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");
123 if (name
== "author")
125 else if (name
== "name")
127 else if (name
== "intro_music") {
130 else if (name
== "main_music") {
133 else if (name
== "on_load") {
134 std::string filename
= value
;
135 if (rc
->resourceExists(filename
)) {
136 onLoadScripts
.push_back(filename
);
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
);
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
);
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
);
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"/>
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
)) {
200 "<tileset>'s width/height contradict earlier <layer>");
203 tileDim
= ivec2(tilex
, tiley
);
205 for (XMLNode child
= node
.childrenNode(); child
; child
= child
.next()) {
206 if (child
.is("image")) {
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);
222 Log::err(descriptor
, "tileset image not found");
226 // Initialize "vanilla" tile type array.
227 BOOST_FOREACH(ImageRef
& tileImg
, img
) {
228 TileType
* type
= new TileType(tileImg
);
230 gids
.push_back(type
);
233 else if (child
.is("tile")) {
234 // Handle an explicitly declared "non-vanilla" type.
238 "Tile type processed before tileset image loaded");
242 // "id" is 0-based index of a tile in the current
243 // tileset, if the tileset were a flat array.
245 ASSERT(child
.intAttr("id", &id
));
247 if (id
< 0 || (int)img
.size() <= id
) {
248 Log::err(descriptor
, "tile type id is invalid");
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
267 bool AreaTMX::processTileType(XMLNode node
, TileType
& type
, TiledImage
& img
, int id
)
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()"/>
281 <property name="members" value="1,2,3,4"/>
282 <property name="speed" value="2"/>
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
)) {
304 "script not found: " + filename
);
307 type
.onEnter
.push_back(filename
);
309 else if (name
== "on_leave") {
310 std::string filename
= value
;
311 if (!rc
->resourceExists(filename
)) {
313 "script not found: " + filename
);
316 type
.onLeave
.push_back(filename
);
318 else if (name
== "on_use") {
319 std::string filename
= value
;
320 if (!rc
->resourceExists(filename
)) {
322 "script not found: " + filename
);
325 type
.onUse
.push_back(filename
);
327 else if (name
== "members") {
329 std::vector
<std::string
> members
;
330 std::vector
<std::string
>::iterator it
;
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.");
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");
351 type
.anim
.addFrame(img
[idx
]);
354 else if (name
== "speed") {
356 ASSERT(child
.doubleAttr("value", &hertz
));
357 int len
= (int)(1000.0/hertz
);
358 type
.anim
.setFrameLen(len
);
365 bool AreaTMX::processLayer(XMLNode node
)
369 <layer name="Tiles0" width="5" height="5">
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");
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));
409 bool AreaTMX::processLayerProperties(XMLNode node
, double* depth
)
414 <property name="layer" value="0"/>
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") {
425 ASSERT(child
.doubleAttr("value", depth
));
426 if (depth2idx
.find(*depth
) != depth2idx
.end()) {
428 "depth used multiple times");
432 depth2idx
[*depth
] = dim
.z
- 1;
433 idx2depth
.push_back(*depth
);
434 // Effectively idx2depth[dim.z - 1] = depth;
439 Log::err(descriptor
, "<layer> must have layer property");
443 bool AreaTMX::processLayerData(XMLNode node
, int z
)
460 for (XMLNode child
= node
.childrenNode(); child
; child
= child
.next()) {
461 if (child
.is("tile")) {
463 ASSERT(child
.intAttr("gid", &gid
));
465 if (gid
< 0 || (int)gids
.size() <= gid
) {
466 Log::err(descriptor
, "invalid tile gid");
470 // A gid of zero means there is no tile at this
471 // position on this layer.
473 TileType
* type
= gids
[gid
];
474 Tile
& tile
= map
[z
][y
][x
];
475 type
->allOfType
.push_back(&tile
);
489 bool AreaTMX::processObjectGroup(XMLNode node
)
493 <objectgroup name="Prop0" width="5" height="5">
495 <property name="layer" value="0.0"/>
497 <object name="tile2" gid="7" x="64" y="320">
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"/>
509 double invalid
= (double)NAN
; // Not a number.
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");
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
));
535 bool AreaTMX::processObjectGroupProperties(XMLNode node
, double* depth
)
540 <property name="layer" value="0.0"/>
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") {
550 ASSERT(child
.doubleAttr("value", depth
));
551 if (depth2idx
.find(*depth
) == depth2idx
.end()) {
553 depth2idx
[*depth
] = dim
.z
- 1;
554 idx2depth
.push_back(*depth
);
555 // Effectively idx2depth[dim.z - 1] = depth;
561 Log::err(descriptor
, "<objectgroup> must have layer property");
565 bool AreaTMX::processObject(XMLNode node
, int z
)
569 <object name="tile2" gid="7" x="64" y="320">
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"/>
578 <object name="foo" x="0" y="0" width="64" height="64">
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
)) {
605 "script not found: " + filename
);
608 onEnter
.push_back(filename
);
610 else if (name
== "on_leave") {
611 std::string filename
= value
;
612 if (!rc
->resourceExists(filename
)) {
614 "script not found: " + filename
);
617 onLeave
.push_back(filename
);
619 else if (name
== "on_use") {
620 std::string filename
= value
;
621 if (!rc
->resourceExists(filename
)) {
623 "script not found: " + filename
);
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") {
651 ASSERT(child
.doubleAttr("value", &mod
));
652 layermods
[EXIT_NORMAL
].reset(mod
);
653 flags
|= TILE_NOWALK_NPC
;
655 else if (name
== "layermod:up") {
657 ASSERT(child
.doubleAttr("value", &mod
));
658 layermods
[EXIT_UP
].reset(mod
);
660 else if (name
== "layermod:down") {
662 ASSERT(child
.doubleAttr("value", &mod
));
663 layermods
[EXIT_DOWN
].reset(mod
);
665 else if (name
== "layermod:left") {
667 ASSERT(child
.doubleAttr("value", &mod
));
668 layermods
[EXIT_LEFT
].reset(mod
);
670 else if (name
== "layermod:right") {
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
681 ASSERT(node
.intAttr("x", &x
));
682 ASSERT(node
.intAttr("y", &y
));
686 if (node
.hasAttr("gid")) {
687 // This is one of Tiled's "Tile Objects". It is one tile wide
689 y
= y
- 1; // Bug in tiled. The y is off by one.
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
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
));
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
];
712 for (int i
= 0; i
< 5; i
++) {
714 tile
.exits
[i
] = new Exit(*exit
[i
].get());
718 tile
.exits
[i
]->coords
.x
+= dx
;
720 tile
.exits
[i
]->coords
.y
+= dy
;
723 for (int i
= 0; i
< 5; i
++)
725 tile
.layermods
[i
] = layermods
[i
];
726 tile
.onEnter
= onEnter
;
727 tile
.onLeave
= onLeave
;
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
) {
741 *flags
|= TILE_NOWALK
;
743 Log::err(descriptor
, "invalid tile flag: " + str
);
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
++) {
755 if (isdigit(c
) || isspace(c
) || c
== '+' ||
756 (c
== '-' && i
== 0))
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");
779 std::string area
= strs
[0],
784 if (!isIntegerOrPlus(xstr
) ||
785 !isIntegerOrPlus(ystr
) ||
786 !isIntegerOrPlus(zstr
)) {
787 Log::err(descriptor
, "<exit />: invalid format");
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
;
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");
815 Gosu::Color::Channel
* channels
[] = { r
, g
, b
, a
};
817 for (int i
= 0; i
< 4; i
++) {
818 std::string s
= strs
[i
];
820 Log::err(descriptor
, "invalid RGBA format");
823 int v
= atoi(s
.c_str());
824 if (!(0 <= v
&& v
< 256)) {
826 "RGBA values must be between 0 and 255");
829 *channels
[i
] = (Gosu::Color::Channel
)v
;