1 /*********************************
2 ** Tsunagari Tile Engine **
4 ** Copyright 2011-2012 OmegaSDG **
5 *********************************/
8 // Permission is hereby granted, free of charge, to any person obtaining a copy
9 // of this software and associated documentation files (the "Software"), to
10 // deal in the Software without restriction, including without limitation the
11 // rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
12 // sell copies of the Software, and to permit persons to whom the Software is
13 // furnished to do so, subject to the following conditions:
15 // The above copyright notice and this permission notice shall be included in
16 // all copies or substantial portions of the Software.
18 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
23 // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
30 #include <boost/foreach.hpp>
31 #include <boost/shared_ptr.hpp>
32 #include <Gosu/Graphics.hpp>
33 #include <Gosu/Image.hpp>
34 #include <Gosu/Math.hpp>
35 #include <Gosu/Timing.hpp>
48 #include "os-windows.h"
51 #define ASSERT(x) if (!(x)) { return false; }
53 /* NOTE: In the TMX map format used by Tiled, tileset tiles start counting
54 their Y-positions from 0, while layer tiles start counting from 1. I
55 can't imagine why the author did this, but we have to take it into
59 AreaTMX::AreaTMX(Viewport
* view
,
61 const std::string
& descriptor
)
62 : Area(view
, player
, descriptor
)
64 // Add TileType #0. Not used, but Tiled's gids start from 1.
74 return processDescriptor();
78 void AreaTMX::allocateMapLayer()
80 map
.push_back(grid_t(dim
.y
, row_t(dim
.x
)));
81 grid_t
& grid
= map
[dim
.z
];
82 for (int y
= 0; y
< dim
.y
; y
++) {
84 for (int x
= 0; x
< dim
.x
; x
++) {
86 new (&tile
) Tile(this, x
, y
, dim
.z
);
92 bool AreaTMX::processDescriptor()
97 ASSERT(doc
= Reader::getXMLDoc(descriptor
, "dtd/area.dtd"));
98 ASSERT(root
= doc
->root()); // <map>
100 ASSERT(root
.intAttr("width", &dim
.x
));
101 ASSERT(root
.intAttr("height", &dim
.y
));
104 for (XMLNode child
= root
.childrenNode(); child
; child
= child
.next()) {
105 if (child
.is("properties")) {
106 ASSERT(processMapProperties(child
));
108 else if (child
.is("tileset")) {
109 ASSERT(processTileSet(child
));
111 else if (child
.is("layer")) {
112 ASSERT(processLayer(child
));
114 else if (child
.is("objectgroup")) {
115 ASSERT(processObjectGroup(child
));
122 bool AreaTMX::processMapProperties(XMLNode node
)
127 <property name="name" value="Wooded AreaTMX"/>
128 <property name="intro_music" value="arrive.ogg"/>
129 <property name="main_music" value="wind.ogg"/>
130 <property name="on_load" value="wood_setup.py"/>
131 <property name="on_focus" value="wood_focus.py"/>
132 <property name="on_tick" value="wood_tick.py"/>
133 <property name="on_turn" value="wood_turn.py"/>
134 <property name="loop" value="xy"/>
135 <property name="color_overlay" value="255,255,255,127"/>
138 for (XMLNode child
= node
.childrenNode(); child
; child
= child
.next()) {
139 std::string name
= child
.attr("name");
140 std::string value
= child
.attr("value");
143 else if (name
== "intro_music") {
146 else if (name
== "main_music") {
149 else if (name
== "on_load") {
150 std::string filename
= value
;
151 ScriptInst
script(filename
);
152 if (!script
.validate(descriptor
))
154 loadScript
= filename
;
156 else if (name
== "on_focus") {
157 std::string filename
= value
;
158 ScriptInst
script(filename
);
159 if (!script
.validate(descriptor
))
161 focusScript
= filename
;
163 else if (name
== "on_tick") {
164 std::string filename
= value
;
165 ScriptInst
script(filename
);
166 if (!script
.validate(descriptor
))
168 tickScript
= filename
;
170 else if (name
== "on_turn") {
171 std::string filename
= value
;
172 ScriptInst
script(filename
);
173 if (!script
.validate(descriptor
))
175 turnScript
= filename
;
177 else if (name
== "loop") {
178 loopX
= value
.find('x') != std::string::npos
;
179 loopY
= value
.find('y') != std::string::npos
;
181 else if (name
== "color_overlay") {
182 Gosu::Color::Channel r
, g
, b
, a
;
183 ASSERT(parseRGBA(value
, &r
, &g
, &b
, &a
));
184 colorOverlay
= Gosu::Color(a
, r
, g
, b
);
191 bool AreaTMX::processTileSet(XMLNode node
)
195 <tileset firstgid="1" name="tiles.sheet" tilewidth="64" tileheight="64">
196 <image source="tiles.sheet" width="256" height="256"/>
211 // Read firstgid from original node.
212 ASSERT(node
.intAttr("firstgid", &firstGid
));
214 // If this node is just a reference to an external TSX file, load it
215 // and process the root tileset element of the TSX, instead.
216 source
= node
.attr("source");
218 if (!(doc
= Reader::getXMLDoc(source
, "dtd/tsx.dtd"))) {
219 Log::err(descriptor
, source
+ ": failed to load valid TSX file");
222 ASSERT(node
= doc
->root()); // <tileset>
225 ASSERT(node
.intAttr("tilewidth", &tilex
));
226 ASSERT(node
.intAttr("tileheight", &tiley
));
228 if (tileDim
&& tileDim
!= ivec2(tilex
, tiley
)) {
230 "<tileset>'s width/height contradict earlier <layer>");
233 tileDim
= ivec2(tilex
, tiley
);
235 for (XMLNode child
= node
.childrenNode(); child
; child
= child
.next()) {
236 if (child
.is("image")) {
238 ASSERT(child
.intAttr("width", &pixelw
) &&
239 child
.intAttr("height", &pixelh
));
240 int width
= pixelw
/ tileDim
.x
;
241 int height
= pixelh
/ tileDim
.y
;
243 std::string source
= child
.attr("source");
244 tileSets
[source
] = TileSet(width
, height
);
245 set
= &tileSets
[source
];
247 // Load tileset image.
248 img
= Reader::getTiledImage(source
, tilex
, tiley
);
250 Log::err(descriptor
, "tileset image not found");
254 // Initialize "vanilla" tile type array.
255 for (size_t i
= 0; i
< img
->size(); i
++) {
256 ImageRef
& tileImg
= (*img
.get())[i
];
257 TileType
* type
= new TileType(tileImg
);
259 gids
.push_back(type
);
262 else if (child
.is("tile")) {
263 // Handle an explicitly declared "non-vanilla" type.
267 "Tile type processed before tileset image loaded");
271 // "id" is 0-based index of a tile in the current
272 // tileset, if the tileset were a flat array.
274 ASSERT(child
.intAttr("id", &id
));
276 if (id
< 0 || (int)img
->size() <= id
) {
277 Log::err(descriptor
, "tile type id is invalid");
281 // Initialize a default TileType, we'll build on that.
282 TileType
* type
= new TileType((*img
.get())[id
]);
283 ASSERT(processTileType(child
, *type
, img
, id
));
285 // "gid" is the global area-wide id of the tile.
286 size_t gid
= id
+ firstGid
;
287 delete gids
[gid
]; // "vanilla" type
296 bool AreaTMX::processTileType(XMLNode node
, TileType
& type
,
297 TiledImageRef
& img
, int id
)
303 <property name="flags" value="nowalk"/>
304 <property name="onEnter" value="skid();speed(2)"/>
305 <property name="onLeave" value="undo()"/>
306 <property name="onUse" value="undo()"/>
311 <property name="frames" value="1,2,3,4"/>
312 <property name="speed" value="2"/>
317 // The id has already been handled by processTileSet, so we don't have
318 // to worry about it.
320 // If a Tile is animated, it needs both member frames and a speed.
321 std::vector
<ImageRef
> framesvec
;
322 int cycles
= ANIM_INFINITE_CYCLES
;
325 XMLNode child
= node
.childrenNode(); // <properties>
326 for (child
= child
.childrenNode(); child
; child
= child
.next()) {
327 // Each <property>...
328 std::string name
= child
.attr("name");
329 std::string value
= child
.attr("value");
330 if (name
== "flags") {
331 ASSERT(splitTileFlags(value
, &type
.flags
));
333 else if (name
== "on_enter") {
334 std::string filename
= value
;
335 ScriptInst
script(filename
);
336 if (!script
.validate(descriptor
))
338 type
.enterScript
= filename
;
340 else if (name
== "on_leave") {
341 std::string filename
= value
;
342 ScriptInst
script(filename
);
343 if (!script
.validate(descriptor
))
345 type
.leaveScript
= filename
;
347 else if (name
== "on_use") {
348 std::string filename
= value
;
349 ScriptInst
script(filename
);
350 if (!script
.validate(descriptor
))
352 type
.useScript
= filename
;
354 else if (name
== "frames") {
356 std::vector
<std::string
> frames
;
357 std::vector
<std::string
>::iterator it
;
359 frames
= splitStr(memtemp
, ",");
361 // Make sure the first member is this tile.
362 if (atoi(frames
[0].c_str()) != id
) {
363 Log::err(descriptor
, "first member of tile"
364 " id " + itostr(id
) +
365 " animation must be itself.");
369 // Add frames to our animation.
370 // We already have one from TileType's constructor.
371 for (it
= frames
.begin(); it
< frames
.end(); it
++) {
372 int idx
= atoi(it
->c_str());
373 if (idx
< 0 || (int)img
->size() <= idx
) {
374 Log::err(descriptor
, "frame index out "
375 "of range for animated tile");
378 framesvec
.push_back((*img
.get())[idx
]);
381 else if (name
== "speed") {
383 ASSERT(child
.doubleAttr("value", &hertz
));
384 frameLen
= (int)(1000.0/hertz
);
386 else if (name
== "cycles") {
387 ASSERT(child
.intAttr("value", &cycles
));
391 if (framesvec
.size() || frameLen
!= -1) {
392 if (framesvec
.empty() || frameLen
== -1) {
393 Log::err(descriptor
, "tile type must either have both "
394 "frames and speed or none");
397 // Add 'now' to Animation constructor??
398 time_t now
= World::instance()->time();
399 type
.anim
= Animation(framesvec
, frameLen
);
400 type
.anim
.startOver(now
, cycles
);
406 bool AreaTMX::processLayer(XMLNode node
)
410 <layer name="Tiles0" width="5" height="5">
428 ASSERT(node
.intAttr("width", &x
));
429 ASSERT(node
.intAttr("height", &y
));
431 if (dim
.x
!= x
|| dim
.y
!= y
) {
432 Log::err(descriptor
, "layer x,y size != map x,y size");
438 for (XMLNode child
= node
.childrenNode(); child
; child
= child
.next()) {
439 if (child
.is("properties")) {
440 ASSERT(processLayerProperties(child
, &depth
));
442 else if (child
.is("data")) {
443 ASSERT(processLayerData(child
, dim
.z
- 1));
450 bool AreaTMX::processLayerProperties(XMLNode node
, double* depth
)
455 <property name="layer" value="0"/>
459 bool layerFound
= false;
461 for (XMLNode child
= node
.childrenNode(); child
; child
= child
.next()) {
462 std::string name
= child
.attr("name");
463 std::string value
= child
.attr("value");
464 if (name
== "layer") {
466 ASSERT(child
.doubleAttr("value", depth
));
467 if (depth2idx
.find(*depth
) != depth2idx
.end()) {
469 "depth used multiple times");
473 depth2idx
[*depth
] = dim
.z
- 1;
474 idx2depth
.push_back(*depth
);
475 // Effectively idx2depth[dim.z - 1] = depth;
480 Log::err(descriptor
, "<layer> must have layer property");
484 bool AreaTMX::processLayerData(XMLNode node
, int z
)
501 for (XMLNode child
= node
.childrenNode(); child
; child
= child
.next()) {
502 if (child
.is("tile")) {
504 ASSERT(child
.intAttr("gid", &gid
));
506 if (gid
< 0 || (int)gids
.size() <= gid
) {
507 Log::err(descriptor
, "invalid tile gid");
511 // A gid of zero means there is no tile at this
512 // position on this layer.
514 TileType
* type
= gids
[gid
];
515 Tile
& tile
= map
[z
][y
][x
];
516 type
->allOfType
.push_back(&tile
);
530 bool AreaTMX::processObjectGroup(XMLNode node
)
534 <objectgroup name="Prop0" width="5" height="5">
536 <property name="layer" value="0.0"/>
538 <object name="tile2" gid="7" x="64" y="320">
540 <property name="onEnter" value="speed(0.5)"/>
541 <property name="onLeave" value="undo()"/>
542 <property name="onUse" value="undo()"/>
543 <property name="exit" value="grassfield.area,1,1,0"/>
544 <property name="flags" value="npc_nowalk"/>
550 double invalid
= (double)NAN
; // Not a number.
552 ASSERT(node
.intAttr("width", &x
));
553 ASSERT(node
.intAttr("height", &y
));
555 double depth
= invalid
;
557 if (dim
.x
!= x
|| dim
.y
!= y
) {
558 Log::err(descriptor
, "objectgroup x,y size != map x,y size");
562 for (XMLNode child
= node
.childrenNode(); child
; child
= child
.next()) {
563 if (child
.is("properties")) {
564 ASSERT(processObjectGroupProperties(child
, &depth
));
566 else if (child
.is("object")) {
567 ASSERT(depth
!= invalid
);
568 int z
= depth2idx
[depth
];
569 ASSERT(processObject(child
, z
));
576 bool AreaTMX::processObjectGroupProperties(XMLNode node
, double* depth
)
581 <property name="layer" value="0.0"/>
584 bool layerFound
= false;
586 for (XMLNode child
= node
.childrenNode(); child
; child
= child
.next()) {
587 std::string name
= child
.attr("name");
588 std::string value
= child
.attr("value");
589 if (name
== "layer") {
591 ASSERT(child
.doubleAttr("value", depth
));
592 if (depth2idx
.find(*depth
) == depth2idx
.end()) {
594 depth2idx
[*depth
] = dim
.z
- 1;
595 idx2depth
.push_back(*depth
);
596 // Effectively idx2depth[dim.z - 1] = depth;
602 Log::err(descriptor
, "<objectgroup> must have layer property");
606 bool AreaTMX::processObject(XMLNode node
, int z
)
610 <object name="tile2" gid="7" x="64" y="320">
612 <property name="onEnter" value="speed(0.5)"/>
613 <property name="onLeave" value="undo()"/>
614 <property name="onUse" value="undo()"/>
615 <property name="exit" value="grassfield.area,1,1,0"/>
616 <property name="flags" value="npc_nowalk"/>
619 <object name="foo" x="0" y="0" width="64" height="64">
624 // Gather object properties now. Assign them to tiles later.
625 bool wwide
[5], hwide
[5]; /* wide exit in dimensions: width, height */
627 ScriptInst enterScript
, leaveScript
, useScript
;
628 boost::scoped_ptr
<Exit
> exit
[5];
629 boost::optional
<double> layermods
[5];
630 unsigned flags
= 0x0;
632 XMLNode child
= node
.childrenNode(); // <properties>
634 // Empty <object> element. Odd, but acceptable.
637 for (child
= child
.childrenNode(); child
; child
= child
.next()) {
638 // Each <property>...
639 std::string name
= child
.attr("name");
640 std::string value
= child
.attr("value");
641 if (name
== "flags") {
642 ASSERT(splitTileFlags(value
, &flags
));
644 else if (name
== "on_enter") {
645 std::string filename
= value
;
646 ScriptInst
script(filename
);
647 if (!script
.validate(descriptor
))
649 enterScript
= script
;
651 else if (name
== "on_leave") {
652 std::string filename
= value
;
653 ScriptInst
script(filename
);
654 if (!script
.validate(descriptor
))
656 leaveScript
= script
;
658 else if (name
== "on_use") {
659 std::string filename
= value
;
660 ScriptInst
script(filename
);
661 if (!script
.validate(descriptor
))
665 else if (name
== "exit") {
666 exit
[EXIT_NORMAL
].reset(new Exit
);
667 ASSERT(parseExit(value
, exit
[EXIT_NORMAL
].get(), &wwide
[EXIT_NORMAL
], &hwide
[EXIT_NORMAL
]));
668 flags
|= TILE_NOWALK_NPC
;
670 else if (name
== "exit:up") {
671 exit
[EXIT_UP
].reset(new Exit
);
672 ASSERT(parseExit(value
, exit
[EXIT_UP
].get(), &wwide
[EXIT_UP
], &hwide
[EXIT_UP
]));
674 else if (name
== "exit:down") {
675 exit
[EXIT_DOWN
].reset(new Exit
);
676 ASSERT(parseExit(value
, exit
[EXIT_DOWN
].get(), &wwide
[EXIT_DOWN
], &hwide
[EXIT_DOWN
]));
678 else if (name
== "exit:left") {
679 exit
[EXIT_LEFT
].reset(new Exit
);
680 ASSERT(parseExit(value
, exit
[EXIT_LEFT
].get(), &wwide
[EXIT_LEFT
], &hwide
[EXIT_LEFT
]));
682 else if (name
== "exit:right") {
683 exit
[EXIT_RIGHT
].reset(new Exit
);
684 ASSERT(parseExit(value
, exit
[EXIT_RIGHT
].get(), &wwide
[EXIT_RIGHT
], &hwide
[EXIT_RIGHT
]));
686 else if (name
== "layermod") {
688 ASSERT(child
.doubleAttr("value", &mod
));
689 layermods
[EXIT_NORMAL
].reset(mod
);
690 flags
|= TILE_NOWALK_NPC
;
692 else if (name
== "layermod:up") {
694 ASSERT(child
.doubleAttr("value", &mod
));
695 layermods
[EXIT_UP
].reset(mod
);
697 else if (name
== "layermod:down") {
699 ASSERT(child
.doubleAttr("value", &mod
));
700 layermods
[EXIT_DOWN
].reset(mod
);
702 else if (name
== "layermod:left") {
704 ASSERT(child
.doubleAttr("value", &mod
));
705 layermods
[EXIT_LEFT
].reset(mod
);
707 else if (name
== "layermod:right") {
709 ASSERT(child
.doubleAttr("value", &mod
));
710 layermods
[EXIT_RIGHT
].reset(mod
);
714 // Apply these properties directly to one or more tiles in a rectangle
715 // of the map. We don't keep an intermediary "object" object lying
718 ASSERT(node
.intAttr("x", &x
));
719 ASSERT(node
.intAttr("y", &y
));
723 if (node
.hasAttr("gid")) {
724 // This is one of Tiled's "Tile Objects". It is one tile wide
727 // Bug in tiled. The y is off by one. The author of the format
728 // knows about this, but it will not change.
733 // We don't actually use the object gid. It is supposed to
734 // indicate which tile our object is rendered as, but for
735 // Tsunagari, tile objects are always transparent and reveal
739 // This is one of Tiled's "Objects". It has a width and height.
740 ASSERT(node
.intAttr("width", &w
));
741 ASSERT(node
.intAttr("height", &h
));
746 // We know which Tiles are being talked about now... yay
747 for (int Y
= y
; Y
< y
+ h
; Y
++) {
748 for (int X
= x
; X
< x
+ w
; X
++) {
749 Tile
& tile
= map
[z
][Y
][X
];
752 for (int i
= 0; i
< 5; i
++) {
754 tile
.exits
[i
] = new Exit(*exit
[i
].get());
758 tile
.exits
[i
]->coords
.x
+= dx
;
760 tile
.exits
[i
]->coords
.y
+= dy
;
763 for (int i
= 0; i
< 5; i
++)
765 tile
.layermods
[i
] = layermods
[i
];
766 tile
.enterScript
= enterScript
;
767 tile
.leaveScript
= leaveScript
;
768 tile
.useScript
= useScript
;
775 bool AreaTMX::splitTileFlags(const std::string
& strOfFlags
, unsigned* flags
)
777 std::vector
<std::string
> strs
= splitStr(strOfFlags
, ",");
779 BOOST_FOREACH(const std::string
& str
, strs
) {
781 *flags
|= TILE_NOWALK
;
782 else if (str
== "nowalk_player")
783 *flags
|= TILE_NOWALK_PLAYER
;
784 else if (str
== "nowalk_npc")
785 *flags
|= TILE_NOWALK_NPC
;
787 Log::err(descriptor
, "invalid tile flag: " + str
);
795 * Matches regex /\s*\d+\+?/
797 static bool isIntegerOrPlus(const std::string
& s
)
805 for (size_t i
= 0; i
< s
.size(); i
++) {
807 if (state
== space
) {
808 if (isspace(c
)) continue;
811 if (state
== digit
) {
812 if (isdigit(c
)) continue;
816 if (c
== '+') return true;
823 bool AreaTMX::parseExit(const std::string
& dest
, Exit
* exit
,
824 bool* wwide
, bool* hwide
)
828 Format: destination area, x, y, z
829 E.g.: "babysfirst.area,1,3,0"
832 std::vector
<std::string
> strs
= splitStr(dest
, ",");
834 if (strs
.size() != 4) {
835 Log::err(descriptor
, "<exit />: invalid format");
839 std::string area
= strs
[0],
844 if (!isIntegerOrPlus(xstr
) ||
845 !isIntegerOrPlus(ystr
) ||
846 !isIntegerOrPlus(zstr
)) {
847 Log::err(descriptor
, "<exit />: invalid format");
852 exit
->coords
.x
= atoi(xstr
.c_str());
853 exit
->coords
.y
= atoi(ystr
.c_str());
854 exit
->coords
.z
= atof(zstr
.c_str());
856 *wwide
= xstr
.find('+') != std::string::npos
;
857 *hwide
= ystr
.find('+') != std::string::npos
;
862 bool AreaTMX::parseRGBA(const std::string
& str
,
863 Gosu::Color::Channel
* r
,
864 Gosu::Color::Channel
* g
,
865 Gosu::Color::Channel
* b
,
866 Gosu::Color::Channel
* a
)
868 std::vector
<std::string
> strs
= splitStr(str
, ",");
870 if (strs
.size() != 4) {
871 Log::err(descriptor
, "invalid RGBA format");
875 Gosu::Color::Channel
* channels
[] = { r
, g
, b
, a
};
877 for (int i
= 0; i
< 4; i
++) {
878 std::string s
= strs
[i
];
880 Log::err(descriptor
, "invalid RGBA format");
883 int v
= atoi(s
.c_str());
884 if (!(0 <= v
&& v
< 256)) {
886 "RGBA values must be between 0 and 255");
889 *channels
[i
] = (Gosu::Color::Channel
)v
;