1 /***************************************
2 ** Tsunagari Tile Engine **
4 ** Copyright 2011-2013 PariahSoft LLC **
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
31 #include <Gosu/Graphics.hpp>
32 #include <Gosu/Image.hpp>
33 #include <Gosu/Math.hpp>
34 #include <Gosu/Timing.hpp>
47 #include "os-windows.h"
50 #define ASSERT(x) if (!(x)) { return false; }
52 /* NOTE: In the TMX map format used by Tiled, tileset tiles start counting
53 their Y-positions from 0, while layer tiles start counting from 1. I
54 can't imagine why the author did this, but we have to take it into
58 AreaTMX::AreaTMX(Viewport
* view
,
60 const std::string
& descriptor
)
61 : Area(view
, player
, descriptor
)
63 // Add TileType #0. Not used, but Tiled's gids start from 1.
73 return processDescriptor();
77 void AreaTMX::allocateMapLayer()
79 map
.push_back(grid_t(dim
.y
, row_t(dim
.x
)));
80 grid_t
& grid
= map
[dim
.z
];
81 for (int y
= 0; y
< dim
.y
; y
++) {
83 for (int x
= 0; x
< dim
.x
; x
++) {
85 new (&tile
) Tile(this, x
, y
, dim
.z
);
91 bool AreaTMX::processDescriptor()
96 ASSERT(doc
= Reader::getXMLDoc(descriptor
, "dtd/area.dtd"));
97 ASSERT(root
= doc
->root()); // <map>
99 ASSERT(root
.intAttr("width", &dim
.x
));
100 ASSERT(root
.intAttr("height", &dim
.y
));
103 for (XMLNode child
= root
.childrenNode(); child
; child
= child
.next()) {
104 if (child
.is("properties")) {
105 ASSERT(processMapProperties(child
));
107 else if (child
.is("tileset")) {
108 ASSERT(processTileSet(child
));
110 else if (child
.is("layer")) {
111 ASSERT(processLayer(child
));
113 else if (child
.is("objectgroup")) {
114 ASSERT(processObjectGroup(child
));
121 bool AreaTMX::processMapProperties(XMLNode node
)
126 <property name="name" value="Wooded AreaTMX"/>
127 <property name="intro_music" value="arrive.ogg"/>
128 <property name="main_music" value="wind.ogg"/>
129 <property name="on_load" value="wood_setup.py"/>
130 <property name="on_focus" value="wood_focus.py"/>
131 <property name="on_tick" value="wood_tick.py"/>
132 <property name="on_turn" value="wood_turn.py"/>
133 <property name="loop" value="xy"/>
134 <property name="color_overlay" value="255,255,255,127"/>
138 musicIntroSet
= false;
139 musicLoopSet
= false;
141 for (XMLNode child
= node
.childrenNode(); child
; child
= child
.next()) {
142 std::string name
= child
.attr("name");
143 std::string value
= child
.attr("value");
146 else if (name
== "intro_music") {
148 musicIntroSet
= true;
150 else if (name
== "main_music") {
154 else if (name
== "on_load") {
155 std::string filename
= value
;
156 ScriptRef script
= Script::create(filename
);
157 if (!script
|| !script
->validate())
161 else if (name
== "on_focus") {
162 std::string filename
= value
;
163 ScriptRef script
= Script::create(filename
);
164 if (!script
|| !script
->validate())
166 focusScript
= script
;
168 else if (name
== "on_tick") {
169 std::string filename
= value
;
170 ScriptRef script
= Script::create(filename
);
171 if (!script
|| !script
->validate())
175 else if (name
== "on_turn") {
176 std::string filename
= value
;
177 ScriptRef script
= Script::create(filename
);
178 if (!script
|| !script
->validate())
182 else if (name
== "loop") {
183 loopX
= value
.find('x') != std::string::npos
;
184 loopY
= value
.find('y') != std::string::npos
;
186 else if (name
== "color_overlay") {
187 Gosu::Color::Channel r
, g
, b
, a
;
188 ASSERT(parseRGBA(value
, &r
, &g
, &b
, &a
));
189 colorOverlay
= Gosu::Color(a
, r
, g
, b
);
196 bool AreaTMX::processTileSet(XMLNode node
)
200 <tileset firstgid="1" name="tiles.sheet" tilewidth="64" tileheight="64">
201 <image source="tiles.sheet" width="256" height="256"/>
216 // Read firstgid from original node.
217 ASSERT(node
.intAttr("firstgid", &firstGid
));
219 // If this node is just a reference to an external TSX file, load it
220 // and process the root tileset element of the TSX, instead.
221 source
= node
.attr("source");
223 if (!(doc
= Reader::getXMLDoc(source
, "dtd/tsx.dtd"))) {
224 Log::err(descriptor
, source
+ ": failed to load valid TSX file");
227 ASSERT(node
= doc
->root()); // <tileset>
230 ASSERT(node
.intAttr("tilewidth", &tilex
));
231 ASSERT(node
.intAttr("tileheight", &tiley
));
233 if (tileDim
&& tileDim
!= ivec2(tilex
, tiley
)) {
235 "<tileset>'s width/height contradict earlier <layer>");
238 tileDim
= ivec2(tilex
, tiley
);
240 for (XMLNode child
= node
.childrenNode(); child
; child
= child
.next()) {
241 if (child
.is("image")) {
243 ASSERT(child
.intAttr("width", &pixelw
) &&
244 child
.intAttr("height", &pixelh
));
245 int width
= pixelw
/ tileDim
.x
;
246 int height
= pixelh
/ tileDim
.y
;
248 std::string source
= child
.attr("source");
249 tileSets
[source
] = TileSet(width
, height
);
250 set
= &tileSets
[source
];
252 // Load tileset image.
253 img
= Reader::getTiledImage(source
, tilex
, tiley
);
255 Log::err(descriptor
, "tileset image not found");
259 // Initialize "vanilla" tile type array.
260 for (size_t i
= 0; i
< img
->size(); i
++) {
261 ImageRef
& tileImg
= (*img
.get())[i
];
262 TileType
* type
= new TileType(tileImg
);
264 gids
.push_back(type
);
267 else if (child
.is("tile")) {
268 // Handle an explicitly declared "non-vanilla" type.
272 "Tile type processed before tileset image loaded");
276 // "id" is 0-based index of a tile in the current
277 // tileset, if the tileset were a flat array.
279 ASSERT(child
.intAttr("id", &id
));
281 if (id
< 0 || (int)img
->size() <= id
) {
282 Log::err(descriptor
, "tile type id is invalid");
286 // Initialize a default TileType, we'll build on that.
287 TileType
* type
= new TileType((*img
.get())[id
]);
288 ASSERT(processTileType(child
, *type
, img
, id
));
290 // "gid" is the global area-wide id of the tile.
291 size_t gid
= id
+ firstGid
;
292 delete gids
[gid
]; // "vanilla" type
301 bool AreaTMX::processTileType(XMLNode node
, TileType
& type
,
302 TiledImageRef
& img
, int id
)
308 <property name="flags" value="nowalk"/>
309 <property name="onEnter" value="skid();speed(2)"/>
310 <property name="onLeave" value="undo()"/>
311 <property name="onUse" value="undo()"/>
316 <property name="frames" value="1,2,3,4"/>
317 <property name="speed" value="2"/>
322 // The id has already been handled by processTileSet, so we don't have
323 // to worry about it.
325 // If a Tile is animated, it needs both member frames and a speed.
326 std::vector
<ImageRef
> framesvec
;
327 int cycles
= ANIM_INFINITE_CYCLES
;
330 XMLNode child
= node
.childrenNode(); // <properties>
331 for (child
= child
.childrenNode(); child
; child
= child
.next()) {
332 // Each <property>...
333 std::string name
= child
.attr("name");
334 std::string value
= child
.attr("value");
335 if (name
== "flags") {
336 ASSERT(splitTileFlags(value
, &type
.flags
));
338 else if (name
== "on_enter") {
339 std::string filename
= value
;
340 ScriptRef script
= Script::create(filename
);
341 if (!script
|| !script
->validate())
343 type
.enterScript
= script
;
345 else if (name
== "on_leave") {
346 std::string filename
= value
;
347 ScriptRef script
= Script::create(filename
);
348 if (!script
|| !script
->validate())
350 type
.leaveScript
= script
;
352 else if (name
== "on_use") {
353 std::string filename
= value
;
354 ScriptRef script
= Script::create(filename
);
355 if (!script
|| !script
->validate())
357 type
.useScript
= script
;
359 else if (name
== "frames") {
361 std::vector
<std::string
> frames
;
362 std::vector
<std::string
>::iterator it
;
364 frames
= splitStr(memtemp
, ",");
366 // Make sure the first member is this tile.
367 if (atoi(frames
[0].c_str()) != id
) {
368 Log::err(descriptor
, "first member of tile"
369 " id " + itostr(id
) +
370 " animation must be itself.");
374 // Add frames to our animation.
375 // We already have one from TileType's constructor.
376 for (it
= frames
.begin(); it
< frames
.end(); it
++) {
377 int idx
= atoi(it
->c_str());
378 if (idx
< 0 || (int)img
->size() <= idx
) {
379 Log::err(descriptor
, "frame index out "
380 "of range for animated tile");
383 framesvec
.push_back((*img
.get())[idx
]);
386 else if (name
== "speed") {
388 ASSERT(child
.doubleAttr("value", &hertz
));
389 frameLen
= (int)(1000.0/hertz
);
391 else if (name
== "cycles") {
392 ASSERT(child
.intAttr("value", &cycles
));
396 if (framesvec
.size() || frameLen
!= -1) {
397 if (framesvec
.empty() || frameLen
== -1) {
398 Log::err(descriptor
, "tile type must either have both "
399 "frames and speed or none");
402 // Add 'now' to Animation constructor??
403 time_t now
= World::instance()->time();
404 type
.anim
= Animation(framesvec
, frameLen
);
405 type
.anim
.startOver(now
, cycles
);
411 bool AreaTMX::processLayer(XMLNode node
)
415 <layer name="Tiles0" width="5" height="5">
433 ASSERT(node
.intAttr("width", &x
));
434 ASSERT(node
.intAttr("height", &y
));
436 if (dim
.x
!= x
|| dim
.y
!= y
) {
437 Log::err(descriptor
, "layer x,y size != map x,y size");
443 for (XMLNode child
= node
.childrenNode(); child
; child
= child
.next()) {
444 if (child
.is("properties")) {
445 ASSERT(processLayerProperties(child
, &depth
));
447 else if (child
.is("data")) {
448 ASSERT(processLayerData(child
, dim
.z
- 1));
455 bool AreaTMX::processLayerProperties(XMLNode node
, double* depth
)
460 <property name="layer" value="0"/>
464 bool layerFound
= false;
466 for (XMLNode child
= node
.childrenNode(); child
; child
= child
.next()) {
467 std::string name
= child
.attr("name");
468 std::string value
= child
.attr("value");
469 if (name
== "layer") {
471 ASSERT(child
.doubleAttr("value", depth
));
472 if (depth2idx
.find(*depth
) != depth2idx
.end()) {
474 "depth used multiple times");
478 depth2idx
[*depth
] = dim
.z
- 1;
479 idx2depth
.push_back(*depth
);
480 // Effectively idx2depth[dim.z - 1] = depth;
485 Log::err(descriptor
, "<layer> must have layer property");
489 bool AreaTMX::processLayerData(XMLNode node
, int z
)
506 for (XMLNode child
= node
.childrenNode(); child
; child
= child
.next()) {
507 if (child
.is("tile")) {
509 ASSERT(child
.intAttr("gid", &gid
));
511 if (gid
< 0 || (int)gids
.size() <= gid
) {
512 Log::err(descriptor
, "invalid tile gid");
516 // A gid of zero means there is no tile at this
517 // position on this layer.
519 TileType
* type
= gids
[gid
];
520 Tile
& tile
= map
[z
][y
][x
];
521 type
->allOfType
.push_back(&tile
);
535 bool AreaTMX::processObjectGroup(XMLNode node
)
539 <objectgroup name="Prop0" width="5" height="5">
541 <property name="layer" value="0.0"/>
543 <object name="tile2" gid="7" x="64" y="320">
545 <property name="onEnter" value="speed(0.5)"/>
546 <property name="onLeave" value="undo()"/>
547 <property name="onUse" value="undo()"/>
548 <property name="exit" value="grassfield.area,1,1,0"/>
549 <property name="flags" value="npc_nowalk"/>
555 double invalid
= (double)NAN
; // Not a number.
557 ASSERT(node
.intAttr("width", &x
));
558 ASSERT(node
.intAttr("height", &y
));
560 double depth
= invalid
;
562 if (dim
.x
!= x
|| dim
.y
!= y
) {
563 Log::err(descriptor
, "objectgroup x,y size != map x,y size");
567 for (XMLNode child
= node
.childrenNode(); child
; child
= child
.next()) {
568 if (child
.is("properties")) {
569 ASSERT(processObjectGroupProperties(child
, &depth
));
571 else if (child
.is("object")) {
572 ASSERT(depth
!= invalid
);
573 int z
= depth2idx
[depth
];
574 ASSERT(processObject(child
, z
));
581 bool AreaTMX::processObjectGroupProperties(XMLNode node
, double* depth
)
586 <property name="layer" value="0.0"/>
589 bool layerFound
= false;
591 for (XMLNode child
= node
.childrenNode(); child
; child
= child
.next()) {
592 std::string name
= child
.attr("name");
593 std::string value
= child
.attr("value");
594 if (name
== "layer") {
596 ASSERT(child
.doubleAttr("value", depth
));
597 if (depth2idx
.find(*depth
) == depth2idx
.end()) {
599 depth2idx
[*depth
] = dim
.z
- 1;
600 idx2depth
.push_back(*depth
);
601 // Effectively idx2depth[dim.z - 1] = depth;
607 Log::err(descriptor
, "<objectgroup> must have layer property");
611 bool AreaTMX::processObject(XMLNode node
, int z
)
615 <object name="tile2" gid="7" x="64" y="320">
617 <property name="onEnter" value="speed(0.5)"/>
618 <property name="onLeave" value="undo()"/>
619 <property name="onUse" value="undo()"/>
620 <property name="exit" value="grassfield.area,1,1,0"/>
621 <property name="flags" value="npc_nowalk"/>
624 <object name="foo" x="0" y="0" width="64" height="64">
629 // Gather object properties now. Assign them to tiles later.
630 bool wwide
[5], hwide
[5]; /* wide exit in dimensions: width, height */
632 ScriptRef enterScript
, leaveScript
, useScript
;
633 std::unique_ptr
<Exit
> exit
[5];
634 std::unique_ptr
<double> layermods
[5];
635 unsigned flags
= 0x0;
637 XMLNode child
= node
.childrenNode(); // <properties>
639 // Empty <object> element. Odd, but acceptable.
642 for (child
= child
.childrenNode(); child
; child
= child
.next()) {
643 // Each <property>...
644 std::string name
= child
.attr("name");
645 std::string value
= child
.attr("value");
646 if (name
== "flags") {
647 ASSERT(splitTileFlags(value
, &flags
));
649 else if (name
== "on_enter") {
650 std::string filename
= value
;
651 ScriptRef script
= Script::create(filename
);
652 if (!script
|| !script
->validate())
654 enterScript
= script
;
656 else if (name
== "on_leave") {
657 std::string filename
= value
;
658 ScriptRef script
= Script::create(filename
);
659 if (!script
|| !script
->validate())
661 leaveScript
= script
;
663 else if (name
== "on_use") {
664 std::string filename
= value
;
665 ScriptRef script
= Script::create(filename
);
666 if (!script
|| !script
->validate())
670 else if (name
== "exit") {
671 exit
[EXIT_NORMAL
].reset(new Exit
);
672 ASSERT(parseExit(value
, exit
[EXIT_NORMAL
].get(), &wwide
[EXIT_NORMAL
], &hwide
[EXIT_NORMAL
]));
673 flags
|= TILE_NOWALK_NPC
;
675 else if (name
== "exit:up") {
676 exit
[EXIT_UP
].reset(new Exit
);
677 ASSERT(parseExit(value
, exit
[EXIT_UP
].get(), &wwide
[EXIT_UP
], &hwide
[EXIT_UP
]));
679 else if (name
== "exit:down") {
680 exit
[EXIT_DOWN
].reset(new Exit
);
681 ASSERT(parseExit(value
, exit
[EXIT_DOWN
].get(), &wwide
[EXIT_DOWN
], &hwide
[EXIT_DOWN
]));
683 else if (name
== "exit:left") {
684 exit
[EXIT_LEFT
].reset(new Exit
);
685 ASSERT(parseExit(value
, exit
[EXIT_LEFT
].get(), &wwide
[EXIT_LEFT
], &hwide
[EXIT_LEFT
]));
687 else if (name
== "exit:right") {
688 exit
[EXIT_RIGHT
].reset(new Exit
);
689 ASSERT(parseExit(value
, exit
[EXIT_RIGHT
].get(), &wwide
[EXIT_RIGHT
], &hwide
[EXIT_RIGHT
]));
691 else if (name
== "layermod") {
693 ASSERT(child
.doubleAttr("value", &mod
));
694 layermods
[EXIT_NORMAL
].reset(new double(mod
));
695 flags
|= TILE_NOWALK_NPC
;
697 else if (name
== "layermod:up") {
699 ASSERT(child
.doubleAttr("value", &mod
));
700 layermods
[EXIT_UP
].reset(new double(mod
));
702 else if (name
== "layermod:down") {
704 ASSERT(child
.doubleAttr("value", &mod
));
705 layermods
[EXIT_DOWN
].reset(new double(mod
));
707 else if (name
== "layermod:left") {
709 ASSERT(child
.doubleAttr("value", &mod
));
710 layermods
[EXIT_LEFT
].reset(new double(mod
));
712 else if (name
== "layermod:right") {
714 ASSERT(child
.doubleAttr("value", &mod
));
715 layermods
[EXIT_RIGHT
].reset(new double(mod
));
719 // Apply these properties directly to one or more tiles in a rectangle
720 // of the map. We don't keep an intermediary "object" object lying
723 ASSERT(node
.intAttr("x", &x
));
724 ASSERT(node
.intAttr("y", &y
));
728 if (node
.hasAttr("gid")) {
729 // This is one of Tiled's "Tile Objects". It is one tile wide
732 // Bug in tiled. The y is off by one. The author of the format
733 // knows about this, but it will not change.
738 // We don't actually use the object gid. It is supposed to
739 // indicate which tile our object is rendered as, but for
740 // Tsunagari, tile objects are always transparent and reveal
744 // This is one of Tiled's "Objects". It has a width and height.
745 ASSERT(node
.intAttr("width", &w
));
746 ASSERT(node
.intAttr("height", &h
));
751 // We know which Tiles are being talked about now... yay
752 for (int Y
= y
; Y
< y
+ h
; Y
++) {
753 for (int X
= x
; X
< x
+ w
; X
++) {
754 Tile
& tile
= map
[z
][Y
][X
];
757 for (int i
= 0; i
< 5; i
++) {
759 tile
.exits
[i
] = new Exit(*exit
[i
].get());
763 tile
.exits
[i
]->coords
.x
+= dx
;
765 tile
.exits
[i
]->coords
.y
+= dy
;
768 for (int i
= 0; i
< 5; i
++)
769 tile
.layermods
[i
] = layermods
[i
] ? new double(*layermods
[i
].get()) : NULL
;
770 tile
.enterScript
= enterScript
;
771 tile
.leaveScript
= leaveScript
;
772 tile
.useScript
= useScript
;
779 bool AreaTMX::splitTileFlags(const std::string
& strOfFlags
, unsigned* flags
)
781 typedef std::vector
<std::string
> StringVector
;
782 StringVector strs
= splitStr(strOfFlags
, ",");
784 for (StringVector::const_iterator it
= strs
.begin(); it
!= strs
.end(); it
++) {
785 const std::string
& str
= *it
;
787 *flags
|= TILE_NOWALK
;
788 else if (str
== "nowalk_player")
789 *flags
|= TILE_NOWALK_PLAYER
;
790 else if (str
== "nowalk_npc")
791 *flags
|= TILE_NOWALK_NPC
;
793 Log::err(descriptor
, "invalid tile flag: " + str
);
801 * Matches regex /\s*\d+\+?/
803 static bool isIntegerOrPlus(const std::string
& s
)
811 for (size_t i
= 0; i
< s
.size(); i
++) {
813 if (state
== space
) {
814 if (isspace(c
)) continue;
817 if (state
== digit
) {
818 if (isdigit(c
)) continue;
822 if (c
== '+') return true;
829 bool AreaTMX::parseExit(const std::string
& dest
, Exit
* exit
,
830 bool* wwide
, bool* hwide
)
834 Format: destination area, x, y, z
835 E.g.: "babysfirst.area,1,3,0"
838 std::vector
<std::string
> strs
= splitStr(dest
, ",");
840 if (strs
.size() != 4) {
841 Log::err(descriptor
, "<exit />: invalid format");
845 std::string area
= strs
[0],
850 if (!isIntegerOrPlus(xstr
) ||
851 !isIntegerOrPlus(ystr
) ||
852 !isIntegerOrPlus(zstr
)) {
853 Log::err(descriptor
, "<exit />: invalid format");
858 exit
->coords
.x
= atoi(xstr
.c_str());
859 exit
->coords
.y
= atoi(ystr
.c_str());
860 exit
->coords
.z
= atof(zstr
.c_str());
862 *wwide
= xstr
.find('+') != std::string::npos
;
863 *hwide
= ystr
.find('+') != std::string::npos
;
868 bool AreaTMX::parseRGBA(const std::string
& str
,
869 Gosu::Color::Channel
* r
,
870 Gosu::Color::Channel
* g
,
871 Gosu::Color::Channel
* b
,
872 Gosu::Color::Channel
* a
)
874 std::vector
<std::string
> strs
= splitStr(str
, ",");
876 if (strs
.size() != 4) {
877 Log::err(descriptor
, "invalid RGBA format");
881 Gosu::Color::Channel
* channels
[] = { r
, g
, b
, a
};
883 for (int i
= 0; i
< 4; i
++) {
884 std::string s
= strs
[i
];
886 Log::err(descriptor
, "invalid RGBA format");
889 int v
= atoi(s
.c_str());
890 if (!(0 <= v
&& v
< 256)) {
892 "RGBA values must be between 0 and 255");
895 *channels
[i
] = (Gosu::Color::Channel
)v
;