aboutsummaryrefslogtreecommitdiff
path: root/include/raytmx.h
diff options
context:
space:
mode:
authorLeander Scherer <leander@schererleander.de>2026-03-08 20:04:08 +0100
committerLeander Scherer <leander@schererleander.de>2026-03-08 20:07:58 +0100
commitc5455ef0fbfc4203a4aa8ad185dfa43bdadc0b82 (patch)
tree731fffe1d9591587a366895f4a46859d2c79436f /include/raytmx.h
parent7bdc10ae6de645812f4e57185067f0a83ca5655f (diff)
feat(deps): add raylib and raytmx dependencies
Diffstat (limited to 'include/raytmx.h')
-rw-r--r--include/raytmx.h4815
1 files changed, 4815 insertions, 0 deletions
diff --git a/include/raytmx.h b/include/raytmx.h
new file mode 100644
index 0000000..f446272
--- /dev/null
+++ b/include/raytmx.h
@@ -0,0 +1,4815 @@
+/*
+Copyright (c) 2024-2026 Luke Philipsen
+
+Permission to use, copy, modify, and/or distribute this software for
+any purpose with or without fee is hereby granted.
+
+THE SOFTWARE IS PROVIDED “AS IS” AND THE AUTHOR DISCLAIMS ALL
+WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES
+OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE
+FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY
+DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN
+AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
+OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+*/
+
+/* Usage
+
+ Do this:
+ #define RAYTMX_IMPLEMENTATION
+ before you include this file in *one* C or C++ file to create the implementation.
+
+ You can define RAYTMX_DEC with
+ #define RAYTMX_DEC static
+ or
+ #define RAYTMX_DEC extern
+ to specify raytmx function declarations as static or extern, respectively.
+ The default specifier is extern.
+*/
+
+#ifndef RAYTMX_H
+ #define RAYTMX_H
+
+#include <ctype.h> /* isspace() */
+#include <math.h> /* floor(), INFINITY */
+#include <stddef.h> /* NULL */
+#include <stdint.h> /* int32_t, uint32_t */
+#include <stdlib.h> /* atoi(), strtoul() */
+#include <string.h> /* memcpy(), memset(), strcpy(), strcpy_s() strlen(), strncpy(), strncpy_s() */
+
+#include "raylib.h"
+#include "rlgl.h"
+
+#ifndef RAYTMX_DEC
+ #define RAYTMX_DEC
+#endif /* RAYTMX_DEC */
+
+#ifdef __cplusplus
+ extern "C" {
+#endif /* __cpluspus */
+
+/***************/
+/* Definitions */
+
+/**
+ * Function signature of raylib's LoadTexture() as a type. For use with SetLoadTextureTMX().
+ */
+typedef Texture2D (*LoadTextureCallback)(const char* fileName);
+
+/**
+ * Bit flags passed to SetTraceLogFlagsTMX() that optionally disable the logging of specific TMX elements.
+ */
+enum tmx_log_flags {
+ LOG_SKIP_PROPERTIES = 1, /**< Skip <properties> and child <property> elements. */
+ LOG_SKIP_LAYERS = 2, /**< Skip <layer>, <objectgroup>, <imagelayer>, and <group> layers and children thereof. */
+ LOG_SKIP_TILE_LAYERS = 4, /**< Skip <layer> layers and children thereof. */
+ LOG_SKIP_TILES = 8, /**< Skip tiles (GIDs) of tile layers (<layer>s). */
+ LOG_SKIP_OBJECT_GROUPS = 16, /**< Skip <objectgroup> layers and children thereof. */
+ LOG_SKIP_OBJECTS = 32, /**< Skip objects of object layers (<objectgroup>s). */
+ LOG_SKIP_IMAGE_LAYERS = 64, /**< Skip <imagelayer> layers and children thereof. */
+ LOG_SKIP_IMAGES = 128, /**< Skip images of image layers (<imagelayer>s). */
+ LOG_SKIP_WANG_SETS = 256, /**< Skip <wangsets> and child <wangset> elements. */
+ LOG_SKIP_WANG_TILES = 512 /**< Skip Wang tiles of Wang sets (<wangset>s). */
+};
+
+/**
+ * Identifiers for the possible layer types.
+ */
+typedef enum tmx_layer_type {
+ LAYER_TYPE_TILE_LAYER = 0, /**< Layer containing a set number of tiles referenced by Global IDs (GIDs). */
+ LAYER_TYPE_OBJECT_GROUP, /**< Layer containing an arbitrary number of various object types. */
+ LAYER_TYPE_IMAGE_LAYER, /**< Layer consisting of a single image. */
+ LAYER_TYPE_GROUP /**< Container layer, with no visuals of its own, that holds other, child layers. */
+} TmxLayerType;
+
+/**
+ * Identifiers for the possible property (data) types.
+ */
+typedef enum tmx_property_type {
+ PROPERTY_TYPE_STRING = 0, /**< String, or a sequence of characters. */
+ PROPERTY_TYPE_INT, /**< Integer number. */
+ PROPERTY_TYPE_FLOAT, /**< Floating-point number. */
+ PROPERTY_TYPE_BOOL, /**< Boolean, true or false. */
+ PROPERTY_TYPE_COLOR, /**< Color with red, green, and blue values and a possible alpha value. */
+ PROPERTY_TYPE_FILE, /**< File name or path to a file as a string. */
+ PROPERTY_TYPE_OBJECT /**< Object (<object>) within the map as an integer ID. */
+} TmxPropertyType;
+
+/**
+ * Identifiers for the possible draw orders applicable to object layers.
+ */
+typedef enum tmx_object_group_draw_order {
+ OBJECT_GROUP_DRAW_ORDER_TOP_DOWN = 0, /**< Drawn in ascending order by y-coordinate. */
+ OBJECT_GROUP_DRAW_ORDER_INDEX /**< Drawn in the order in which the appear in the document. */
+} TmxObjectGroupDrawOrder;
+
+/**
+ * Identifiers for the possible alignments of tiles with an object's bounds.
+ */
+typedef enum tmx_object_alignment {
+ OBJECT_ALIGNMENT_UNSPECIFIED = 0, /**< For orthogonal, behaves like bottom-left. For isometric, like bottm. */
+ OBJECT_ALIGNMENT_TOP_LEFT, /**< Tiles are snapped to the upper-left bound of objects. */
+ OBJECT_ALIGNMENT_TOP, /**< Tiles are snapped to the upper-center bound of objects. */
+ OBJECT_ALIGNMENT_TOP_RIGHT, /**< Tiles are snapped to the upper-right bound of objects. */
+ OBJECT_ALIGNMENT_LEFT, /**< Tiles are snapped to the left-center bound of objects. */
+ OBJECT_ALIGNMENT_CENTER, /**< Tiles are snapped to the horizontal and vertical center of objects. */
+ OBJECT_ALIGNMENT_RIGHT, /**< Tiles are snapped to the right-center bound of objects. */
+ OBJECT_ALIGNMENT_BOTTOM_LEFT, /**< Tiles are snapped to the lower-left bound of objects. */
+ OBJECT_ALIGNMENT_BOTTOM, /**< Tiles are snapped to the lower-center bound of objects. */
+ OBJECT_ALIGNMENT_BOTTOM_RIGHT /**< Tiles are snapped to the lower-right bound of objects. */
+} TmxObjectAlignment;
+
+/**
+ * Identifiers for the possible object types.
+ */
+typedef enum tmx_object_type {
+ OBJECT_TYPE_RECTANGLE = 0, /**< Four-sided polygon four right angles and axis-aligned edges. */
+ OBJECT_TYPE_ELLIPSE, /**< Ellipse, or a circle when the axes are equal. */
+ OBJECT_TYPE_POINT, /**< Individual (X, Y) coordinate with no dimensions. */
+ OBJECT_TYPE_POLYGON, /**< Filled polygon formed by an ordered series of points. */
+ OBJECT_TYPE_POLYLINE, /**< Unfilled polygon formed by an ordered series of points. */
+ OBJECT_TYPE_TEXT, /**< Text, or the visual representation of a string. */
+ OBJECT_TYPE_TILE /**< Tile, referenced by a Global ID (GID), from a tileset known to the map. */
+} TmxObjectType;
+
+/**
+ * Identifiers for the possible horizontal alignments of text.
+ */
+typedef enum tmx_horizontal_alignment {
+ HORIZONTAL_ALIGNMENT_LEFT = 0, /**< Text is to be snapped to the left bound of its object. */
+ HORIZONTAL_ALIGNMENT_CENTER, /**< Text is to be centered along the X axis of its object. */
+ HORIZONTAL_ALIGNMENT_RIGHT, /**< Text is to be snapped to the right bound of its object. */
+ HORIZONTAL_ALIGNMENT_JUSTIFY /**< Text is to be evenly spaced, per line, filling the object's width. */
+} TmxHorizontalAlignment;
+
+/**
+ * Identifiers for the possible vertical alignments of text.
+ */
+typedef enum tmx_vertical_alignment {
+ VERTICAL_ALIGNMENT_TOP = 0, /**< Text is to be snapped to the upper bound of its object. */
+ VERTICAL_ALIGNMENT_CENTER, /**< Text is to be cnetered along the Y axis of its object. */
+ VERTICAL_ALIGNMENT_BOTTOM /**< Text is to be snapped to the lower bound of its object. */
+} TmxVerticalAlignment;
+
+/**
+ * Identifiers for the possible map orientations.
+ */
+typedef enum tmx_orientation {
+ ORIENTATION_NONE = 0, /**< Orientation was not specified. Assumed to be orthogonal. */
+ ORIENTATION_ORTHOGONAL, /**< Standard top-down view with rectanglular tiles. */
+ ORIENTATION_ISOMETRIC, /**< Subset top-down view from a 45-degree angle. NOT supported. */
+ ORIENTATION_STAGGERED, /**< Variation of isometric with staggered axes. */
+ ORIENTATION_HEXAGONAL /**< Top-down view in which tiles are six-sided and alternative rows/columns are offset. */
+} TmxOrientation;
+
+/**
+ * Identifiers for the possible orders in which tiles in a tile layer are rendered/drawn.
+ */
+typedef enum tmx_render_order {
+ RENDER_ORDER_RIGHT_DOWN = 0, /**< Tiles are rendered by row, from left to right, then column, from top to bottom. */
+ RENDER_ORDER_RIGHT_UP, /**< Tiles are rendered by row, from left to right, then column, from bottom to top. */
+ RENDER_ORDER_LEFT_DOWN, /**< Tiles are rendered by row, from right to left, then column, from top to bottom. */
+ RENDER_ORDER_LEFT_UP /**< Tiles are rendered by row, from right to left, then column, from bottom to top. */
+} TmxRenderOrder;
+
+/**
+ * Model of an <image> element. Defines an image and relevant attributes along with a loaded texture.
+ */
+typedef struct tmx_image {
+ char* source; /**< File name and/or path referencing the image on disk. */
+ Color trans; /**< [optional] Color that defines is treated as transparent. Not currently implemented. */
+ bool hasTrans; /**< When true, indicates 'trans' has been set with a color to be treated as transparent. */
+ uint32_t width; /**< Width of the image in pixels. */
+ uint32_t height; /**< Height of the image in pixels. */
+ Texture2D texture; /**< The image as a raylib texture loaded into VRAM, if loading was successful. */
+} TmxImage;
+
+/**
+ * Model of a <layer> element when combined with the 'TmxLayer' model. Defines a tile layer with a fixed-size list of
+ * tile Global IDs (GIDs).
+ */
+typedef struct tmx_tile_layer {
+ uint32_t width; /**< Width of the layer in tiles. */
+ uint32_t height; /**< Height of the layer in tiles. */
+ char* encoding; /**< [optional] Encoding used to encode tiles. May be NULL, "base64," or "csv." */
+ char* compression; /**< [optional] Compression used to compress tiles. May be NULL, "gzip," "zlib," or "zstd." */
+ uint32_t* tiles; /**< Array of tile Global IDs (GIDs) contained by this tile layer. */
+ uint32_t tilesLength; /**< Length of the 'tiles' array. */
+} TmxTileLayer;
+
+/**
+ * Contains the information needed to quickly draw a single line of a <text> element.
+ */
+typedef struct tmx_text_line {
+ char* content; /**< The string to be drawn. This may be the whole content of the parent string or partial. */
+ Font font; /**< The raylib Font to be used when drawing. */
+ Vector2 position; /**< Absolute position of this line. This is separate from its object layer's potential offset. */
+ float spacing; /**< Spacing in pixels to be applied between each character when drawing. */
+} TmxTextLine;
+
+/**
+ * Model of a <text> element along with some pre-calculated objects for efficient drawing.
+ */
+typedef struct tmx_text {
+ char* fontFamily; /**< Font family (e.g. "sans-serif") to be used to render the text. */
+ uint32_t pixelSize; /**< Size of the font in pixels. */
+ bool wrap; /**< When true, indicates word wrapping should be used when appropriate. */
+ Color color; /**< Color of the text. */
+ bool bold; /**< When true, indicates the text should be bolded. */
+ bool italic; /**< When true, indicates the text should be italicized. */
+ bool underline; /**< When true, indicates the text should be underlined. */
+ bool strikeOut; /**< When true, indicates the text should be struck/crossed out. */
+ bool kerning; /**< When true, indicates kerning should be used when drawing. */
+ TmxHorizontalAlignment halign; /**< Horizontal alignment of the text within its object. */
+ TmxVerticalAlignment valign; /**< Vertical alignment of the text within its object. */
+ char* content; /**< The string to be drawn. */
+ TmxTextLine* lines; /**< Array of pre-calculated lines with all values needed to quickly draw this text. */
+ uint32_t linesLength; /**< Length of the 'lines' array. */
+} TmxText;
+
+/**
+ * Model of a <property> element. Describes a property of the model it's attached to with a name, type, and value.
+ */
+typedef struct tmx_property {
+ TmxPropertyType type; /**< The specific (data) type of the property indicating which associated value to read. */
+ char* name; /**< Name of the property. */
+ char* stringValue; /**< The property's value for string-typed properties. */
+ int32_t intValue; /**< The property's value for integer-typed properties. */
+ float floatValue; /**< The property's value for floating point-typed properties. */
+ bool boolValue; /**< The property's value for boolean-typed properties. */
+ Color colorValue; /**< The property's value for color-typed properties. */
+} TmxProperty;
+
+/**
+ * Model of an <object> element within an <objectgroup> element. Objects are amorphous entities of varying type but all
+ * are potentially visible with positions and dimensions, although points' dimensions are effectively zero.
+ */
+typedef struct tmx_object {
+ TmxObjectType type; /**< The specific object type indicating which optional fields have relevant information. */
+ uint32_t id; /**< Unique ID of the object. */
+ char* name; /**< Name of the object. */
+ char* typeString; /**< The type/class of the object. */ /* 'type' is a reserved keyword hence 'typeString' */
+ double x; /**< X coordinate, in pixels, of the object. This is separate from its object layer's potential offset. */
+ double y; /**< Y coordinate, in pixels, of the object. This is separate from its object layer's potential offset. */
+ double width; /**< Width of the object in pixels. */
+ double height; /**< Height of the object in pixels. */
+ double rotation; /**< Rotation of the object in (clockwise) degrees around the object's (x, y) position. */
+ uint32_t gid; /**< (Semi-optional) Global ID of a tile drawn as the object. If zero, the object is not a tile. */
+ bool visible; /**< When true, indicates the object will be drawn. */
+ char* templateString; /**< [optional] File name and/or path referencing an object template on disk applied to this
+ object. If NULL, no template is used. */
+ Vector2* points; /**< [optional] Array of ordered points that define a poly(gon|line). Relative, not absolute. */
+ uint32_t pointsLength; /**< Length of the 'points' array. */
+ Vector2* drawPoints; /**< [optional] Array used as a buffer when drawing. Equal in length to the 'points' array. */
+ TmxText* text; /**< [optional] Text to be drawn. */
+ TmxProperty* properties; /**< Array of named, typed properties that apply to this object. */
+ uint32_t propertiesLength; /**< Length of the 'properties' array. */
+ Rectangle aabb; /**< Axis-Aligned Bounding Box (AABB). */
+} TmxObject;
+
+/**
+ * Model of an <objectgroup> element when combined with the 'TmxLayer' model. Defines an object layer of an arbitrary
+ * number of objects of varying types.
+ */
+typedef struct tmx_object_group {
+ /* uint32_t width; */ /**< Width of the object layer in tiles. TMX documentation describes it as "meaningless." */
+ /* uint32_t height; */ /**< Height of the object layer in tiles. TMX documentation describes it as "meaningless." */
+ Color color; /**< [optional] Color used to display objects within the layer. */
+ bool hasColor; /**< When true, indicates 'color' has been set. */
+ TmxObjectGroupDrawOrder drawOrder; /**< Indicates the order in which objects in this layer are drawn. */
+ TmxObject* objects; /**< Array of objects contained by this object layer. */
+ uint32_t objectsLength; /**< Length of the 'objects' array. */
+ uint32_t* ySortedObjects; /**< Array of indexes of 'objects' sorted by the objects' y-coordinates. */
+} TmxObjectGroup;
+
+/**
+ * Model of an <imagelayer> element when combined with the 'TmxLayer' model. Defines a layer consisting of one image.
+ */
+typedef struct tmx_image_layer {
+ bool repeatX; /**< When true, indicates the image is repeated along the X axis. */
+ bool repeatY; /**< When true, indicates the image is repeated along the Y axis. */
+ TmxImage image; /**< Sole image of this layer. */
+ bool hasImage; /**< When true, indicates 'image' has been set. Should always be true. */
+} TmxImageLayer;
+
+struct tmx_layer; /* Forward declaration of the following type. Contains children of the same type. */
+
+/**
+ * Model of multiple layer elements: <layer>, <objectgroup>, <imagelayer>, or <group>. Defines a layer with attributes
+ * common to all, more-specific layer types. The more-specific attributes
+ */
+typedef struct tmx_layer {
+ TmxLayerType type; /**< The specific layer type indicating which associated layer ('exact') has mspecific values. */
+ uint32_t id; /**< Unique integer ID of the layer. */
+ char* name; /**< Name of the layer. */
+ char* classString; /**< [optional] Class of the layer. If unused, defaults to an empty string. */
+ bool visible; /**< When true, indicates the layer and its children will be drawn. */
+ double opacity; /**< Opacity of the layer and its children where 0.0 means the layer is fully transparent. */
+ Color tintColor; /**< [optional] Tint color applied to the layer and its children. */
+ bool hasTintColor; /**< When true, indicates 'tintColor' has been set. */
+ int32_t offsetX; /**< Horizontal offset of the layer and its children in pixels. */
+ int32_t offsetY; /**< Vertical offset of the layer and its children in pixels. */
+ double parallaxX; /**< Horizontal parallax factor. 1.0 means the layers position on the screen changes at the same
+ rate as the camera. 0.0 means the layer will not move with the camera. */
+ double parallaxY; /**< Veritcal parallax factor. 1.0 means the layers position on the screen changes at the same
+ rate as the camera. 0.0 means the layer will not move with the camera. */
+ TmxProperty* properties; /**< Array of named, typed properties that apply to this layer. */
+ uint32_t propertiesLength; /**< Length of the 'properties' array. */
+ struct tmx_layer* layers; /**< [optional] Array of child layers, may be NULL. Only used by group layers. */
+ uint32_t layersLength; /**< Length of the 'layers' array. */
+ union layer_type_union {
+ TmxTileLayer tileLayer;
+ TmxObjectGroup objectGroup;
+ TmxImageLayer imageLayer;
+ } exact; /**< Additional layer information specific to a tile, object, or image layer but not groups. */
+} TmxLayer;
+
+/**
+ * Model of a <frame> element. Defines a temporal frame of an animation with the Global ID (GID) of the tile to be
+ * displayed and the duration thereof.
+ */
+typedef struct tmx_animation_frame {
+ uint32_t gid; /**< The Global ID (GID) of a tile from the animation's tileset to be drawn for some duration. */
+ float duration; /**< Duration in milliseconds that the frame should be displayed. */
+} TmxAnimationFrame;
+
+/**
+ * Model of an <animation> element. Defines a series of (tile) frames.
+ */
+typedef struct tmx_animation {
+ TmxAnimationFrame* frames; /**< Array of frames. These frames identify tiles and durations to be displayed. */
+ uint32_t framesLength; /**< Length of the 'frames' array. */
+} TmxAnimation;
+
+/**
+ * Model of a <tile> element within a <tileset> element. Contains information about tiles that are not or cannot be
+ * implicitly determined from the tileset.
+ */
+typedef struct tmx_tileset_tile {
+ uint32_t id; /**< Local ID of the tile within its tileset. This is a factor in but different from its Global ID. */
+ char* classString; /**< [optional] Class of the tile. If unused, defaults to an empty string. */
+ int32_t x; /**< X coordinate, in pixels, of the sub-rectangle within the tileset's image to extract. */
+ int32_t y; /**< Y coordinate, in pixels, of the sub-rectangle within the tileset's image to extract. */
+ uint32_t width; /**< Width, in pixels, of the sub-rectangle within the tileset's image to extract. */
+ uint32_t height; /**< Height, in pixels, of the sub-rectangle within the tileset's image to extract. */
+ TmxImage image; /**< [optional] Image to be used as the tile for "collection of images" tilesets. */
+ bool hasImage; /**< When true, indicates 'image' is set. */
+ TmxAnimation animation; /**< [optional] Animation. Lists GIDs to be drawn temporarily and periodically. */
+ bool hasAnimation; /**< When true, indicates 'animation' is assigned. */
+ TmxProperty* properties; /**< Array of named, typed properties that apply to this tileset tile. */
+ uint32_t propertiesLength; /**< Length of the 'properties' array. */
+ TmxObjectGroup objectGroup; /**< [optional] 0+ objects representing collision information unique to the tile. */
+} TmxTilesetTile;
+
+/**
+ * Model of a <tileset> element. Defines an image, or serious of images, from which tiles are drawn along with
+ * information on how to extract areas from within the image and/or how to align them within an object.
+ */
+typedef struct tmx_tileset {
+ uint32_t firstGid; /**< First Global ID (GID) of a tile in this tileset. */
+ uint32_t lastGid; /**< Last Global ID (GID) of a tile in this tileset. */
+ char* source; /**< [optional] Source of this tileset, may be NULL. Only used for external tilesets. */
+ char* name; /**< Name of the tileset. */
+ char* classString; /**< [optional] Class of the tileset. If unused, defaults to an empty string. */
+ uint32_t tileWidth; /**< Maximum, although typically exact, width of the tiles in this tileset in pixels. */
+ uint32_t tileHeight; /**< Maximum, although typically exact, height of the tiles in this tileset in pixels. */
+ uint32_t spacing; /**< Spacing in pixels between tiles in this tileset. */
+ uint32_t margin; /**< Margin around the tiles in this tileset. */
+ uint32_t tileCount; /**< Number of tiles in this tileset. Note: 'lastGid' - 'firstGid' is not always 'tileCount.' */
+ uint32_t columns; /**< Number of tile columsn in this tileset. */
+ TmxObjectAlignment objectAlignment; /**< Controls the alignment of tiles of this tileset when used as objects. */
+ int32_t tileOffsetX; /**< Horizontal offset in pixels applied when drawing tiles from this tileset. */
+ int32_t tileOffsetY; /**< Vertical offset in pixels applied when drawing tiles form this tileset. */
+ TmxImage image; /**< [optional] Image from which this tilesets tiles are extracted. */
+ bool hasImage; /**< When true, indicates 'image' is set. */
+ TmxProperty* properties; /**< Array of named, typed properties that apply to this tileset. */
+ uint32_t propertiesLength; /**< Length of the 'properties' array. */
+ TmxTilesetTile* tiles; /**< Array of explicitly-defined tiles within the tileset. */
+ uint32_t tilesLength; /**< Length of the 'tiles' array. */
+} TmxTileset;
+
+/**
+ * Contains the information and objects needed to quickly draw a <tile> in a raylib application.
+ */
+typedef struct tmx_tile {
+ uint32_t gid; /**< Global ID (GID) unique to the tile, used for lookups. 0 means the tile is effectively unused. */
+ Rectangle sourceRect; /**< Sub-rectangle within a tileset to extract that is to be drawn. */
+ Texture2D texture; /**< Texture in VRAM to be used to draw. May be used whole or as a source of a sub-rectangle. */
+ Vector2 offset; /**< Offset in pixels to be applied to the tile, derived from the tileset. */
+ TmxAnimation animation; /**< [optional] Animation. Lists GIDs to be drawn temporarily and periodically. */
+ bool hasAnimation; /**< When true, indicates 'animation' is assigned. */
+ uint32_t frameIndex; /**< For animations, the current animation frame to draw. */
+ float frameTime; /**< For animations, an accumulator. The time, in seconds, the current frame has been drawn. */
+ TmxObjectGroup objectGroup; /**< [optional] 0+ objects representing collision information unique to the tile. */
+} TmxTile;
+
+/**
+ * Model of a <map> element along with some pre-calculated objects for efficient drawing.
+ */
+typedef struct tmx_map {
+ char* fileName; /**< File name of the TMX file with extension. */
+ TmxOrientation orientation; /**< Map orientation. May be orthogonal, isometric, staggered, or hexagonal. */
+ TmxRenderOrder renderOrder; /**< Order in which tiles on tile layers are rendered. */
+ uint32_t width; /**< Width of this map in tiles. */
+ uint32_t height; /**< Height of htis map in tiles. */
+ uint32_t tileWidth; /**< Width of a tile in pixels. */
+ uint32_t tileHeight; /**< Height of a tile in pixels. */
+ int32_t parallaxOriginX; /**< X coordinate, in pixels, of the parallax origin. */
+ int32_t parallaxOriginY; /**< Y coordinate, in pixels, of hte parallax origin. */
+ Color backgroundColor; /**< [optional] Background color to be drawn behind the map with its dimensions. */
+ bool hasBackgroundColor; /**< When true, indicates 'backgroundColor' is set. */
+ TmxProperty* properties; /**< Array of named, typed properties that apply to this map. */
+ uint32_t propertiesLength; /**< Length of the 'properties' array. */
+ TmxTileset* tilesets; /**< Array of tilesets used by the map. */
+ uint32_t tilesetsLength; /**< Length of the 'tilesets' array. */
+ TmxLayer* layers; /**< Array of layers and potential child layers that make up this map. */
+ uint32_t layersLength; /**< Length of the 'layers' array. */
+ TmxTile* gidsToTiles; /**< Array of pre-calculated tile metadata with all the values needed to quickly draw a tile
+ given its GID. Allocated such that gidsToTiles[1] returns the data of tile GID 1. */
+ uint32_t gidsToTilesLength; /**< Length of the 'gidsToTiles' array. */
+} TmxMap;
+
+/**
+ * Given a path to TMX document, parse it and create an equivalent model that can be, among other uses, quickly drawn.
+ * This function allocates memory and loads textures into VRAM. To clean up, use UnloadTMX().
+ *
+ * @param fileName File name and/or path referencing a TMX document on disk to be loaded.
+ * @return A model of the map as defined by the given TMX document, or NULL if loading failed for any reason.
+ */
+RAYTMX_DEC TmxMap* LoadTMX(const char* fileName);
+
+/**
+ * Unload a given map model by freeing memory allocations and unloading textures. In other words, free the resources
+ * reserved by LoadTMX().
+ *
+ * @param map A previously-loaded map model to be freed/unloaded.
+ */
+RAYTMX_DEC void UnloadTMX(TmxMap* map);
+
+/**
+ * Draw the entirety of the given map at the given position.
+ * When a camera is also passed to this function, parallaxed scrolling can be applied to layers with parallax factors
+ * that are not 1.0 and the screen's surface can be known allowing for occlusion culling and the performance gains that
+ * come with it.
+ * The given tint is applied to map and its layers. When layers have their own tints, the two colors are combined. If no
+ * tint is needed, passing WHITE effectively means no tint is applied.
+ *
+ * @param map A loaded map model to be drawn in whole at the given coordinates.
+ * @param camera [optional] Camera2D to be used for parallax and/or occlusion. Required for parallax. 'viewport' takes
+ * priority for occlusion.
+ * @param viewport [optional] Region drawn to. Used for occlusion. Only tiles, objects, etc. in this region are drawn.
+ * @param posX X coordinate at which to draw the map. This corresponds to the top-left corner of the map.
+ * @param posY Y coordinate at which to draw the map. This corresponds to the top-left corner of the map.
+ * @param tint A tint to be applied to the map and its layers. This tint is combined with any individual layer tints.
+ */
+RAYTMX_DEC void DrawTMX(const TmxMap* map, const Camera2D* camera, const Rectangle* viewport, int posX, int posY,
+ Color tint);
+
+/**
+ * Draw the given layers at the given position.
+ * When a camera is also passed to this function, parallaxed scrolling can be applied to layers with parallax factors
+ * that are not 1.0 and the screen's surface can be known allowing for occlusion culling and the performance gains that
+ * come with it.
+ * The given tint is applied to the layers. When layers have their own tints, the two colors are combined. If no tint is
+ * needed, passing WHITE effectively means no tint is applied.
+ *
+ * @param map A loaded map model whose layers are to be drawn.
+ * @param camera [optional] Camera2D to be used for parallax and/or occlusion. Required for parallax. 'viewport' takes
+ * priority for occlusion.
+ * @param viewport [optional] Region drawn to. Used for occlusion. Only tiles, objects, etc. in this region are drawn.
+ * @param layers An array of select layers to be drawn.
+ * @param layersLength Length of the given array of layers.
+ * @param posX X coordinate at which to draw the layers. This corresponds to the top-left corner of the layers.
+ * @param posY Y coordinate at which to draw the layers. This corresponds to the top-left corner of the layers.
+ * @param tint A tint to be applied to the layers. This tint is combined with any individual layer tints.
+ */
+RAYTMX_DEC void DrawTMXLayers(const TmxMap* map, const Camera2D* camera, const Rectangle* viewport,
+ const TmxLayer* layers, uint32_t layersLength, int posX, int posY, Color tint);
+
+/**
+ * Progress the animations of the given map in real-time. This is intended to be called once per frame, or once per
+ * BeginDrawing() an EndDrawing() call. If called more or less frequently, animation speeds will be affected.
+ *
+ * @param map A loaded map model to be animated.
+ */
+RAYTMX_DEC void AnimateTMX(TmxMap* map);
+
+/**
+ * Check for collisions between two objects of arbitrary type. Objects that are not primitive shapes, namely text and
+ * tiles, are treated as rectangles.
+ *
+ * @param object1 A TMX <object> to be checked for a collision.
+ * @param object2 Another TMX <object> to be checked for a collision.
+ * @return True if the given objects collide with one another, or false if there is no collision.
+ */
+RAYTMX_DEC bool CheckCollisionTMXObjects(TmxObject object1, TmxObject object2);
+
+/**
+ * Check for collisions between the given tile or group layers and the given rectangle. The tiles must have collision
+ * information created with the Tiled Collision Editor.
+ * Note: This function assumes the map is positioned at (0, 0). If the map is drawn with an offset, normalize.
+ *
+ * @param map A loaded map model containing the given layers.
+ * @param layers An array of select tile or group layers to be checked for collisions.
+ * @param layersLength Length of the given array of layers.
+ * @param rec The rectangle to perform collision checks on.
+ * @param outputObject Output parameter assigned with the object the rectangle collided with. NULL if not wanted.
+ * @return True if the given rectangle collides with any tile in the given layers, or false if there is no collision.
+ */
+RAYTMX_DEC bool CheckCollisionTMXTileLayersRec(const TmxMap* map, const TmxLayer* layers, uint32_t layersLength,
+ Rectangle rec, TmxObject* outputObject);
+
+/**
+ * Check for collisions between the given tile or group layers and the given circle. The tiles must have collision
+ * information created with the Tiled Collision Editor.
+ * Note: This function assumes the map is positioned at (0, 0). If the map is drawn with an offset, normalize.
+ *
+ * @param map A loaded map model containing the given layers.
+ * @param layers An array of select tile or group layers to be checked for collisions.
+ * @param layersLength Length of the given array of layers.
+ * @param center The center point of the circle.
+ * @param radius The radius of the circle.
+ * @param outputObject Output parameter assigned with the object the circle collided with. NULL if not wanted.
+ * @return True if the given circle collides with any tile in the given layers, or false if there is no collision.
+ */
+RAYTMX_DEC bool CheckCollisionTMXTileLayersCircle(const TmxMap* map, const TmxLayer* layers, uint32_t layersLength,
+ Vector2 center, float radius, TmxObject* outputObject);
+
+/**
+ * Check for collisions between the given tile or group layers and the given point. The tiles must have collision
+ * information created with the Tiled Collision Editor.
+ * Note: This function assumes the map is positioned at (0, 0). If the map is drawn with an offset, normalize.
+ *
+ * @param map A loaded map model containing the given layers.
+ * @param layers An array of select tile or group layers to be checked for collisions.
+ * @param layersLength Length of the given array of layers.
+ * @param point The point to perform collision checks on.
+ * @param outputObject Output parameter assigned with the object the point collided with. NULL if not wanted.
+ * @return True if the given point collides with any tile in the given layers, or false if there is no collision.
+ */
+RAYTMX_DEC bool CheckCollisionTMXTileLayersPoint(const TmxMap* map, const TmxLayer* layers, uint32_t layersLength,
+ Vector2 point, TmxObject* outputObject);
+
+/**
+ * Check for collisions between the given tile or group layers and the given polygon. The tiles must have collision
+ * information created with the Tiled Collision Editor.
+ * Note: This function assumes the map is positioned at (0, 0). If the map is drawn with an offset, normalize.
+ *
+ * This function calculates the Axis-Aligned Bounding Box (AABB) of the polygon each time it's called. If the polygon's
+ * AABB is known, CheckCollisionTMXLayersPolyEx() can be used for better performance.
+ *
+ * @param map A loaded map model containing the given layers.
+ * @param layers An array of select tile or group layers to be checked for collisions.
+ * @param layersLength Length of the given array of layers.
+ * @param points An array of vertices defining the polygon. No repeats.
+ * @param pointCount The length of the array of vertices.
+ * @param outputObject Output parameter assigned with the object the polygon collided with. NULL if not wanted.
+ * @return True if the given polygon collides with any tile in the given layers, or false if there is no collision.
+ */
+RAYTMX_DEC bool CheckCollisionTMXLayersPoly(const TmxMap* map, const TmxLayer* layers, uint32_t layersLength,
+ Vector2* points, int pointCount, TmxObject* outputObject);
+
+/**
+ * Check for collisions between the given tile or group layers and the given polygon with the given Axis-Aligned
+ * Bounding Box (AABB). The tiles must have collision information created with the Tiled Collision Editor.
+ * Note: This function assumes the map is positioned at (0, 0). If the map is drawn with an offset, normalize.
+ *
+ * @param map A loaded map model containing the given layers.
+ * @param layers An array of select tile or group layers to be checked for collisions.
+ * @param layersLength Length of the given array of layers.
+ * @param points An array of vertices defining the polygon. No repeats.
+ * @param pointCount The length of the array of vertices.
+ * @param aabb Bounding box of the polygon. Used for quicker, broad collision checks.
+ * @param outputObject Output parameter assigned with the object the polygon collided with. NULL if not wanted.
+ * @return True if the given polygon collides with any tile in the given layers, or false if there is no collision.
+ */
+RAYTMX_DEC bool CheckCollisionTMXLayersPolyEx(const TmxMap* map, const TmxLayer* layers, uint32_t layersLength,
+ Vector2* points, int pointCount, Rectangle aabb, TmxObject* outputObject);
+
+/**
+ * Check for collisions between the given object group, with 0+ objects of arbitrary shape, and the given rectangle.
+ * Note: This function assumes the map is positioned at (0, 0). If the map is drawn with an offset, normalize.
+ *
+ * @param group The object group whose 0+ objects will be checked for collisions.
+ * @param rec The rectangle to perform collision checks on.
+ * @param outputObject Output parameter assigned with the object the rectangle collided with. NULL if not wanted.
+ * @return True if the given rectangle collides with any object in the object group, or false if there is no collision.
+ */
+RAYTMX_DEC bool CheckCollisionTMXObjectGroupRec(TmxObjectGroup group, Rectangle rec, TmxObject* outputObject);
+
+/**
+ * Check for collisions between the given object group, with 0+ objects of arbitrary shape, and the given circle.
+ * Note: This function assumes the map is positioned at (0, 0). If the map is drawn with an offset, normalize.
+ *
+ * @param group The object group whose 0+ objects will be checked for collisions.
+ * @param center The center point of the circle.
+ * @param radius The radius of the circle.
+ * @param outputObject Output parameter assigned with the object the circle collided with. NULL if not wanted.
+ * @return True if the given circle collides with any object in the object group, or false if there is no collision.
+ */
+RAYTMX_DEC bool CheckCollisionTMXObjectGroupCircle(TmxObjectGroup group, Vector2 center, float radius,
+ TmxObject* outputObject);
+
+/**
+ * Check for collisions between the given object group, with 0+ objects of arbitrary shape, and the given point.
+ * Note: This function assumes the map is positioned at (0, 0). If the map is drawn with an offset, normalize.
+ *
+ * @param group The object group whose 0+ objects will be checked for collisions.
+ * @param point The point to perform collision checks on.
+ * @param outputObject Output parameter assigned with the object the point collided with. NULL if not wanted.
+ * @return True if the given point collides with any object in the object group, or false if there is no collision.
+ */
+RAYTMX_DEC bool CheckCollisionTMXObjectGroupPoint(TmxObjectGroup group, Vector2 point, TmxObject* outputObject);
+
+/**
+ * Check for collisions between the given object group, with 0+ objects of arbitrary shape, and the given polygon.
+ * This function calculates the Axis-Aligned Bounding Box (AABB) of the polygon each time it's called. If the polygon's
+ * AABB is known, CheckCollisionTMXObjectGroupPolyEx() can be used for better performance.
+ * Note: This function assumes the map is positioned at (0, 0). If the map is drawn with an offset, normalize.
+ *
+ * @param group The object group whose 0+ objects will be checked for collisions.
+ * @param points An array of vertices defining the polygon. No repeats.
+ * @param pointCount The length of the array of vertices.
+ * @param outputObject Output parameter assigned with the object the polygon collided with. NULL if not wanted.
+ * @return True if the given polygon collides with any object in the object group, or false if there is no collision.
+ */
+RAYTMX_DEC bool CheckCollisionTMXObjectGroupPoly(TmxObjectGroup group, Vector2* points, int pointCount,
+ TmxObject* outputObject);
+
+/**
+ * Check for collisions between the given object group, with 0+ objects of arbitrary shape, and the given polygon with
+ * the given Axis-Aligned Bound Box (AABB).
+ * Note: This function assumes the map is positioned at (0, 0). If the map is drawn with an offset, normalize.
+ *
+ * @param group The object group whose 0+ objects will be checked for collisions.
+ * @param points An array of vertices defining the polygon. No repeats.
+ * @param pointCount The length of the array of vertices.
+ * @param aabb Bounding box of the polygon. Used for quicker, broad collision checks.
+ * @param outputObject Output parameter assigned with the object the polygon collided with. NULL if not wanted.
+ * @return True if the given polygon collides with any object in the object group, or false if there is no collision.
+ */
+RAYTMX_DEC bool CheckCollisionTMXObjectGroupPolyEx(TmxObjectGroup group, Vector2* points, int pointCount,
+ Rectangle aabb, TmxObject* outputObject);
+
+/**
+ * Set a custom callback in place of raylib's LoadTexture(). The callback must return a Texture2D and take a const char*
+ * as the sole parameter. To unset, pass NULL to this function.
+ *
+ * @param callback A function pointer with a "Texture2D CustLoadTexture(const char* fileName)" signature, or NULL if
+ * unsetting the custom callback.
+ */
+RAYTMX_DEC void SetLoadTextureTMX(LoadTextureCallback callback);
+
+/**
+ * Log properties of the given map as a formatted string.
+ * SetTraceLogFlagsTMX() may be used to exclude select information.
+ *
+ * @param logLevel The level/severity with which to log the string (e.g. LOG_DEBUG, LOG_INFO, etc.).
+ * @param map A loaded map to be logged.
+ */
+RAYTMX_DEC void TraceLogTMX(int logLevel, const TmxMap* map);
+
+/**
+ * Globally set logging options for TraceLogTMX() allowing for select types of information to be excluded.
+ * The flags used by this function are defined in the tmx_log_flags enumeration.
+ *
+ * @param logFlags Logically OR'd bit flags to be applied to all logging following this call.
+ */
+RAYTMX_DEC void SetTraceLogFlagsTMX(int logFlags);
+
+#ifdef __cplusplus
+ }
+#endif /* __cplusplus */
+
+#ifdef RAYTMX_IMPLEMENTATION
+
+#ifndef HOXML_IMPLEMENTATION
+ #define HOXML_IMPLEMENTATION
+#endif
+#include "hoxml.h"
+
+/******************/
+/* Implementation */
+
+#define TMX_LINE_THICKNESS 3.0f /* Thickness, in pixels, that outlines of specific objects are drawn with */
+
+/* Bit flags that GIDs may be masked with in order to indicate transformations for individual tiles */
+enum tmx_flip_flags {
+ FLIP_FLAG_HORIZONTAL = 0x80000000,
+ FLIP_FLAG_VERTICAL = 0x40000000,
+ FLIP_FLAG_DIAGONAL = 0x20000000,
+ FLIP_FLAG_ROTATE_120 = 0x10000000
+};
+
+typedef enum raytmx_document_format {
+ FORMAT_TMX = 0, /* Tilemap with tilesets, layers, etc. */
+ FORMAT_TSX, /* External tilesets */
+ FORMAT_TX /* Object templates */
+} RaytmxDocumentFormat;
+
+typedef struct raytmx_external_tileset {
+ TmxTileset tileset;
+ bool isSuccess; /* 'isSuccess' is true when the external tileset was successfully loaded */
+} RaytmxExternalTileset;
+
+typedef struct raytmx_object_template {
+ TmxTileset tileset;
+ TmxObject object;
+ bool isSuccess, hasTileset; /* 'isSuccess' is true when the object template was successfully loaded */
+} RaytmxObjectTemplate;
+
+struct raytmx_cached_texture; /* Forward declaration */
+typedef struct raytmx_cached_texture {
+ char* fileName;
+ Texture2D texture;
+ struct raytmx_cached_texture* next;
+} RaytmxCachedTextureNode; /* Associates a file name with a Texture2D allowing for the reuse of textures in VRAM */
+
+struct raytmx_cached_template; /* Forward declaration */
+typedef struct raytmx_cached_template {
+ char* fileName;
+ RaytmxObjectTemplate objectTemplate;
+ struct raytmx_cached_template* next;
+} RaytmxCachedTemplateNode; /* Associates a file name with an object template */
+
+struct raytmx_property_node; /* Forward declaration */
+typedef struct raytmx_property_node {
+ TmxProperty property;
+ struct raytmx_property_node* next;
+} RaytmxPropertyNode;
+
+struct raytmx_tileset_node; /* Forward declaration */
+typedef struct raytmx_tileset_node {
+ TmxTileset tileset;
+ struct raytmx_tileset_node* next;
+} RaytmxTilesetNode;
+
+struct raytmx_tileset_tile_node; /* Forward declaration */
+typedef struct raytmx_tileset_tile_node {
+ TmxTilesetTile tile;
+ struct raytmx_tileset_tile_node* next;
+} RaytmxTilesetTileNode;
+
+struct raytmx_animation_frame_node; /* Forward declaration */
+typedef struct raytmx_animation_frame_node {
+ TmxAnimationFrame frame;
+ struct raytmx_animation_frame_node* next;
+} RaytmxAnimationFrameNode;
+
+struct raytmx_layer_node; /* Forward declaration */
+typedef struct raytmx_layer_node {
+ TmxLayer layer;
+ uint32_t childrenLength;
+ struct raytmx_layer_node *next, *parent, *childrenRoot, *childrenTail;
+} RaytmxLayerNode;
+
+struct raytmx_tile_layer_tile_node; /* Forward declaration */
+typedef struct raytmx_tile_layer_tile_node {
+ uint32_t gid;
+ struct raytmx_tile_layer_tile_node* next;
+} RaytmxTileLayerTileNode;
+
+struct raytmx_object_node; /* Forward declaration */
+typedef struct raytmx_object_node {
+ TmxObject object;
+ struct raytmx_object_node* next;
+} RaytmxObjectNode;
+
+struct raytmx_object_sorting_node; /* Forward declaration */
+typedef struct raytmx_object_sorting_node {
+ double y;
+ uint32_t index;
+ struct raytmx_object_sorting_node* next;
+} RaytmxObjectSortingNode;
+
+struct raytmx_poly_point_node; /* Forward declaration */
+typedef struct raytmx_poly_point_node {
+ Vector2 point;
+ struct raytmx_poly_point_node* next;
+} RaytmxPolyPointNode;
+
+struct raytmx_text_line_node; /* Forward declaration */
+typedef struct raytmx_text_line_node {
+ TmxTextLine line;
+ struct raytmx_text_line_node* next;
+} RaytmxTextLineNode;
+
+typedef struct raytmx_state {
+ RaytmxDocumentFormat format;
+ char documentDirectory[512];
+ bool isSuccess;
+
+ /* Variables intended for TMX (map) parsing */
+ RaytmxCachedTextureNode* texturesRoot;
+ RaytmxCachedTemplateNode* templatesRoot;
+ TmxOrientation mapOrientation;
+ TmxRenderOrder mapRenderOrder;
+ uint32_t mapWidth, mapHeight, mapTileWidth, mapTileHeight, mapPropertiesLength;
+ int32_t mapParallaxOriginX, mapParallaxOriginY;
+ Color mapBackgroundColor;
+ bool mapHasBackgroundColor;
+ TmxProperty* mapProperties;
+
+ /* These variables, when not NULL, are assigned to the current element(s) being parsed */
+ TmxProperty* property;
+ TmxTileset* tileset;
+ TmxImage* image;
+ TmxTilesetTile* tilesetTile;
+ TmxAnimationFrame* animationFrame;
+ /* TmxWangSet* wangSet; */ /* TODO: Wang sets. Low priority. */
+ /* TmxWangColor* wangColor; */ /* TODO: Wang sets. Low priority. */
+ TmxLayer* layer;
+ TmxTileLayer* tileLayer;
+ TmxObjectGroup* objectGroup;
+ TmxImageLayer* imageLayer;
+ TmxObject* object;
+
+ /* These variables are linked lists containing various elements where an arbitrary amount are allowed, such as */
+ /* 1+ <object> elements in an <objectgroup>, that will be copied to arrays of known sizes later on */
+ RaytmxPropertyNode *propertiesRoot, *propertiesTail;
+ RaytmxTilesetNode *tilesetsRoot, *tilesetsTail;
+ RaytmxTilesetTileNode *tilesetTilesRoot, *tilesetTilesTail;
+ RaytmxAnimationFrameNode *animationFramesRoot, *animationFramesTail;
+ RaytmxLayerNode *layersRoot, *layersTail, *groupNode;
+ RaytmxTileLayerTileNode *layerTilesRoot, *layerTilesTail;
+ RaytmxObjectNode *objectsRoot, *objectsTail;
+ uint32_t tilesetsLength, tilesetTilesLength, animationFramesLength, propertiesLength, layersLength,
+ layerTilesLength, objectsLength, propertiesDepth;
+} RaytmxState; /* Intermediate data used internally to parse TMX (map), TSX (tileset), and TX (template) files */
+
+typedef struct raytmx_transform {
+ Vector2 position;
+ Vector2 parallax;
+ Vector2 cameraOffset;
+} RaytmxTransform;
+
+RaytmxExternalTileset LoadTSX(const char* fileName);
+RaytmxObjectTemplate LoadTX(const char* fileName);
+void ParseDocument(RaytmxState* raytmxState, const char* fileName);
+void HandleElementBegin(RaytmxState* raytmxState, hoxml_context_t* hoxmlContext);
+void HandleAttribute(RaytmxState* raytmxState, hoxml_context_t* hoxmlContext);
+void HandleElementEnd(RaytmxState* raytmxState, hoxml_context_t* hoxmlContext);
+void FreeState(RaytmxState* raytmxState);
+void FreeString(char* str);
+void FreeTileset(TmxTileset tileset);
+void FreeProperty(TmxProperty property);
+void FreeLayer(TmxLayer layer);
+void FreeObject(TmxObject object);
+bool IterateTileLayer(const TmxMap* map, const TmxTileLayer* layer, Rectangle viewport, RaytmxTransform transform,
+ uint32_t* rawGid, TmxTile* tile, Rectangle* tileRect);
+void DrawTMXLayersInternal(const TmxMap* map, const Camera2D* camera, const Rectangle* viewport, const TmxLayer* layers,
+ uint32_t layersLength, RaytmxTransform transform, Color tint);
+void DrawTMXTileLayer(const TmxMap* map, Rectangle viewport, TmxLayer layer, RaytmxTransform transform, Color tint);
+void DrawTMXLayerTile(const TmxMap* map, Rectangle viewport, uint32_t rawGid, Rectangle destRect, Color tint);
+void DrawTMXObjectTile(const TmxMap* map, Rectangle viewport, uint32_t rawGid, RaytmxTransform transform, float width,
+ float height, Color tint);
+void DrawTMXObjectGroup(const TmxMap* map, Rectangle viewport, TmxLayer layer, RaytmxTransform transform, Color tint);
+void DrawTMXImageLayer(const TmxMap* map, Rectangle viewport, TmxLayer layer, RaytmxTransform transform, Color tint);
+bool CheckCollisionTMXTileLayerObject(const TmxMap* map, const TmxLayer* layers, uint32_t layersLength,
+ TmxObject object, TmxObject* outputObject);
+bool CheckCollisionTMXObjectGroupObject(TmxObjectGroup group, TmxObject object, TmxObject* outputObject);
+void TraceLogTMXTilesets(int logLevel, TmxOrientation orientation, TmxTileset* tilesets, uint32_t tilesetsLength,
+ int numSpaces);
+void TraceLogTMXProperties(int logLevel, TmxProperty* properties, uint32_t propertiesLength, int numSpaces);
+void TraceLogTMXLayers(int logLevel, TmxLayer* layers, uint32_t layersLength, int numSpaces);
+void TraceLogTMXObject(int logLevel, TmxObject object, int numSpaces);
+void StringCopy(char* destination, const char* source);
+TmxProperty* AddProperty(RaytmxState* raytmxState);
+void AddTileLayerTile(RaytmxState* raytmxState, uint32_t gid);
+TmxTileset* AddTileset(RaytmxState* raytmxState);
+TmxTilesetTile* AddTilesetTile(RaytmxState* raytmxState);
+TmxAnimationFrame* AddAnimationFrame(RaytmxState* raytmxState);
+TmxLayer* AddGenericLayer(RaytmxState* raytmxState, bool isGroup);
+TmxObject* AddObject(RaytmxState* raytmxState);
+void AppendLayerTo(TmxMap* map, RaytmxLayerNode* groupNode, RaytmxLayerNode* layersRoot, uint32_t layersLength);
+RaytmxCachedTextureNode* LoadCachedTexture(RaytmxState* raytmxState, const char* fileName);
+RaytmxCachedTemplateNode* LoadCachedTemplate(RaytmxState* raytmxState, const char* fileName);
+Color GetColorFromHexString(const char* hex);
+uint32_t GetGid(uint32_t rawGid, bool* isFlippedHorizontally, bool* isFlippedVertically, bool* isFlippedDiagonally,
+ bool* isRotatedHexagonal120);
+void* MemAllocZero(unsigned int size);
+char* GetDirectoryPath2(const char* filePath);
+char* JoinPath(const char* prefix, const char* suffix);
+void StringCopyN(char* destination, const char* source, size_t number);
+void StringConcatenate(char* destination, const char* source);
+
+/**********************************************************************************************************************/
+/* Public implementation. */
+
+RAYTMX_DEC TmxMap* LoadTMX(const char* fileName) {
+ RaytmxState raytmxState[1];
+ memset(raytmxState, 0, sizeof(RaytmxState)); /* Initialize all values to zero, NULL, or an equivalent enum value */
+ raytmxState->format = FORMAT_TMX;
+
+ /* Initialize the map object */
+ TmxMap* map = (TmxMap*)MemAllocZero(sizeof(TmxMap));
+
+ /* Do format-agnostic parsing of the document. The state object will be populated with raytmx's models of the */
+ /* equivalent TMX, TSX, and/or TX elements. */
+ ParseDocument(raytmxState, fileName);
+ if (!raytmxState->isSuccess) {
+ UnloadTMX(map);
+ return NULL;
+ }
+
+ /* Copy some top-level map properties */
+ map->fileName = (char*)MemAllocZero((unsigned int)strlen(fileName) + 1);
+ StringCopy(map->fileName, GetFileName(fileName));
+ map->orientation = raytmxState->mapOrientation;
+ map->renderOrder = raytmxState->mapRenderOrder;
+ map->width = raytmxState->mapWidth;
+ map->height = raytmxState->mapHeight;
+ map->tileWidth = raytmxState->mapTileWidth;
+ map->tileHeight = raytmxState->mapTileHeight;
+ map->backgroundColor = raytmxState->mapBackgroundColor;
+ map->parallaxOriginX = raytmxState->mapParallaxOriginX;
+ map->parallaxOriginY = raytmxState->mapParallaxOriginY;
+ map->hasBackgroundColor = raytmxState->mapHasBackgroundColor;
+
+ uint32_t gidsToTilesLength = 0; /* Can also be seen as the last GID */
+ if (raytmxState->tilesetsRoot != NULL) { /* If there is at least one tileset */
+ /* Allocate the array of tilesets and zeroize every index */
+ TmxTileset* tilesets = (TmxTileset*)MemAllocZero(sizeof(TmxTileset) * raytmxState->tilesetsLength);
+ /* Copy the TmxTileset pointers into the array */
+ RaytmxTilesetNode* tilesetIterator = raytmxState->tilesetsRoot;
+ for (uint32_t i = 0; tilesetIterator != NULL; i++) {
+ TmxTileset tileset = tilesetIterator->tileset;
+
+ if (tileset.hasImage) { /* If the tileset has a shared image and implicitly defines tiles */
+ /* These tilesets implicitly have X tiles where X = <width in tiles> * <height in tiles>. But even */
+ /* these tilesets may have explicitly defined tiles for things like animations, or anything else. We */
+ /* need to know the greatest ID of those explicit tiles because they are allowed to have IDs that */
+ /* exceed the tileset's tile count and this affects global IDs, even in other tilesets. */
+ uint32_t greatestID = 0;
+ for (uint32_t i = 0; i < tileset.tilesLength; i++)
+ {
+ if (greatestID < tileset.tiles[i].id)
+ greatestID = tileset.tiles[i].id;
+ }
+ /* Determine the last GID, meaning the greatest GID value, from the greatest local ID and tile count */
+ if (greatestID >= tileset.tileCount)
+ tileset.lastGid = tileset.firstGid + greatestID;
+ else
+ tileset.lastGid = tileset.firstGid + tileset.tileCount - 1;
+ } else if (tileset.tilesLength > 0) /* If the tileset is a "collection of images" with explicit tiles */
+ tileset.lastGid = tileset.firstGid + tileset.tiles[tileset.tilesLength - 1].id - 1;
+
+ if (gidsToTilesLength < tileset.lastGid + 1)
+ gidsToTilesLength = tileset.lastGid + 1; /* GIDs start at 1 so the length is the last GID + 1 */
+ tilesets[i] = tileset;
+ tilesetIterator = tilesetIterator->next;
+ }
+ /* Add the tilesets array to the map */
+ map->tilesets = tilesets;
+ map->tilesetsLength = raytmxState->tilesetsLength;
+ } else
+ TraceLog(LOG_WARNING, "RAYTMX: The map does not contain any tilesets");
+
+ if (raytmxState->layersRoot != NULL) { /* If there is at least one layer within the map */
+ /* Due to the existence of <group> layers, layers can have children of multiple generations. To form the */
+ /* resulting tree-like structure, recursion is used. */
+ AppendLayerTo(map, NULL, raytmxState->layersRoot, raytmxState->layersLength);
+ } else
+ TraceLog(LOG_WARNING, "RAYTMX: The map does not contain any layers");
+
+ if (gidsToTilesLength > 0) {
+ TmxTile* gidsToTiles = (TmxTile*)MemAllocZero(sizeof(TmxTile) * gidsToTilesLength);
+
+ for (uint32_t i = 0; i < map->tilesetsLength; i++) {
+ TmxTileset* tileset = &map->tilesets[i];
+ if (tileset->hasImage) { /* If the tileset has a shared image (i.e. not a "collection of images") */
+ /* First, iterate through the explicit tiles. These are explicitly defined with <tile> elements and */
+ /* are used to, among other things, provide animations and specific source rectangles. Note: Their */
+ /* IDs may exceed the tileset's tile count. */
+ for (uint32_t j = 0; j < tileset->tilesLength; j++) {
+ TmxTilesetTile tilesetTile = tileset->tiles[j];
+ uint32_t gid = tileset->firstGid + tilesetTile.id;
+ if (tilesetTile.hasAnimation) { /* If the tile is meta, pointing to a series of other tiles */
+ gidsToTiles[gid].hasAnimation = true;
+ gidsToTiles[gid].animation = tilesetTile.animation;
+ /* Frames' tile (G)IDs are initially set with local values. Now that all tilesets are known */
+ /* and this tileset's first global ID is available, update the ID to be global. */
+ for (uint32_t k = 0; k < gidsToTiles[gid].animation.framesLength; k++)
+ gidsToTiles[gid].animation.frames[k].gid += tileset->firstGid;
+ }
+ if (tilesetTile.x != 0 || tilesetTile.y != 0 || tilesetTile.width != 0 || tilesetTile.height != 0) {
+ /* This tile directly tells us the area within the tileset's image to use when drawing, */
+ /* overriding the implicit dimensions derived from the map's tile width and height. */
+ gidsToTiles[gid].sourceRect.x = (float)tilesetTile.x;
+ gidsToTiles[gid].sourceRect.y = (float)tilesetTile.y;
+ gidsToTiles[gid].sourceRect.width = (float)tilesetTile.width;
+ gidsToTiles[gid].sourceRect.height = (float)tilesetTile.height;
+ }
+ /* Tiles may have child object groups. These objects are a form of collision information. The */
+ /* object group may be empty or may have objects. A simple assignment covers both. */
+ gidsToTiles[gid].objectGroup = tilesetTile.objectGroup;
+ gidsToTiles[gid].gid = gid; /* Tell the tile its GID */
+ } /* for (uint32_t j = 0; j < tileset->tilesLength; j++) */
+ /* Second, loop through the implicit tiles. These are inferred from knowing the dimensions of the */
+ /* tileset's image, dimensiosn of tiles, and the (right-down) order of tiles within the image. */
+ for (uint32_t id = 0; id < tileset->tileCount; id++) {
+ uint32_t gid = id + tileset->firstGid, x = id % tileset->columns, y = id / tileset->columns;
+ gidsToTiles[gid].gid = gid; /* Tell the tile its GID */
+ /* If that source renctangle within the image was NOT explicitly defined. */
+ /* Note: Float comparisons like this are typically unreliable but the GIDs-to-tiles array is */
+ /* initialized with zeroes making this accurate. */
+ if (gidsToTiles[gid].sourceRect.x == 0.0f && gidsToTiles[gid].sourceRect.y == 0.0f &&
+ gidsToTiles[gid].sourceRect.width == 0.0f && gidsToTiles[gid].sourceRect.height == 0.0f) {
+ /* Calculate the area within the texture to be drawn from contextual information */
+ gidsToTiles[gid].sourceRect.x = (float)(tileset->margin + (x * tileset->tileWidth) +
+ (x * tileset->spacing));
+ gidsToTiles[gid].sourceRect.y = (float)(tileset->margin + (y * tileset->tileHeight) +
+ (y * tileset->spacing));
+ gidsToTiles[gid].sourceRect.width = (float)tileset->tileWidth;
+ gidsToTiles[gid].sourceRect.height = (float)tileset->tileHeight;
+ }
+ gidsToTiles[gid].texture = tileset->image.texture;
+ gidsToTiles[gid].offset.x = (float)tileset->tileOffsetX;
+ gidsToTiles[gid].offset.y = (float)tileset->tileOffsetY;
+ } /* for (uint32_t id = 0; id < tileset->tileCount; id++) */
+ } else { /* If the tileset is a collection of images where each tile has its own image */
+ for (uint32_t j = 0; j < tileset->tilesLength; j++) {
+ TmxTilesetTile tilesetTile = tileset->tiles[j];
+ if (!tilesetTile.hasImage) {
+ TraceLog(LOG_WARNING, "RAYTMX: Skipping tile %d of image collection tileset \"%s\" because "
+ "it has no image", tilesetTile.id, tileset->name);
+ continue;
+ }
+
+ int32_t gid = tileset->firstGid + tilesetTile.id;
+ gidsToTiles[gid].gid = gid;
+ gidsToTiles[gid].sourceRect.x = (float)tilesetTile.x; /* Defaults to and probably is zero */
+ gidsToTiles[gid].sourceRect.y = (float)tilesetTile.y; /* Defaults to and probably is zero */
+ if (tilesetTile.width != tilesetTile.image.width)
+ gidsToTiles[gid].sourceRect.width = (float)tilesetTile.width;
+ else
+ gidsToTiles[gid].sourceRect.width = (float)tilesetTile.image.width;
+ if (tilesetTile.height != tilesetTile.image.height)
+ gidsToTiles[gid].sourceRect.height = (float)tilesetTile.height;
+ else
+ gidsToTiles[gid].sourceRect.height = (float)tilesetTile.image.height;
+ gidsToTiles[gid].texture = tilesetTile.image.texture;
+ }
+ }
+ }
+
+ map->gidsToTiles = gidsToTiles;
+ map->gidsToTilesLength = gidsToTilesLength;
+ } /* gidsToTilesLength > 0 */
+
+ /* Free the linked lists and zeroize related values */
+ FreeState(raytmxState);
+
+ return map;
+}
+
+RAYTMX_DEC void UnloadTMX(TmxMap* map) {
+ if (map == NULL)
+ return;
+
+ if (map->fileName != NULL)
+ MemFree(map->fileName);
+
+ if (map->properties != NULL) {
+ for (uint32_t i = 0; i < map->propertiesLength; i++)
+ FreeProperty(map->properties[i]);
+ MemFree(map->properties);
+ }
+
+ if (map->tilesets != NULL) {
+ for (uint32_t i = 0; i < map->tilesetsLength; i++)
+ FreeTileset(map->tilesets[i]);
+ MemFree(map->tilesets);
+ }
+
+ if (map->layers != NULL) {
+ for (uint32_t i = 0; i < map->layersLength; i++)
+ FreeLayer(map->layers[i]);
+ MemFree(map->layers);
+ }
+
+ if (map->gidsToTiles != NULL)
+ MemFree(map->gidsToTiles);
+
+ MemFree(map);
+}
+
+RAYTMX_DEC void DrawTMX(const TmxMap* map, const Camera2D* camera, const Rectangle* viewport, int posX, int posY,
+ Color tint) {
+ if (map == NULL)
+ return;
+
+ if (map->hasBackgroundColor) {
+ Rectangle backgroundRect;
+ if (viewport != NULL)
+ backgroundRect = *viewport;
+ else {
+ backgroundRect.x = (float)posX;
+ backgroundRect.y = (float)posY;
+ backgroundRect.width = (float)(map->width * map->tileWidth);
+ backgroundRect.height = (float)(map->height * map->tileHeight);
+ }
+
+ DrawRectangleRec(/* rec: */ backgroundRect, /* color: */ map->backgroundColor);
+ }
+
+ DrawTMXLayers(map, camera, viewport, map->layers, map->layersLength, posX, posY, tint);
+}
+
+RAYTMX_DEC void DrawTMXLayers(const TmxMap* map, const Camera2D* camera, const Rectangle* viewport,
+ const TmxLayer* layers, uint32_t layersLength, int posX, int posY, Color tint)
+{
+ /* Pack some position and related information into an object to pass to various functions. At a minimum, this */
+ /* will determine the position each layer is drawn at. With a camera, this will also be used for parallax. */
+ RaytmxTransform transform =
+ {
+ .position = (Vector2){ .x = (float)posX, .y = (float)posY },
+ .parallax = (Vector2){ .x = 1.0f, .y = 1.0f },
+ .cameraOffset = (Vector2){ .x = 0.0, .y = 0.0f }
+ };
+
+ if (map != NULL && camera != NULL) /* If the map, with its parallax origin, and a camera are available */
+ {
+ /* Calculate the camera's offset, or distance, from the parallax origin. This origin is the reference point */
+ /* for parallax scrolling and this offset determines how much a layer will scroll as the camera moves. */
+ transform.cameraOffset = (Vector2)
+ {
+ .x = camera->target.x - map->parallaxOriginX,
+ .y = camera->target.y - map->parallaxOriginY
+ };
+ }
+
+ DrawTMXLayersInternal(map, camera, viewport, layers, layersLength, transform, tint);
+}
+
+RAYTMX_DEC void AnimateTMX(TmxMap* map) {
+ if (map == NULL)
+ return;
+
+ float dt = GetFrameTime(); /* Returns the duration, in seconds, of the last frame drawn */
+ /* Iterate through the tiles, searching for those that are animations */
+ for (uint32_t gid = 0; gid < map->gidsToTilesLength; gid++) {
+ TmxTile* tile = &map->gidsToTiles[gid]; /* A pointer is used in case the frame time needs to be reassigned */
+ /* If the GID maps to a valid tile, that tile is an animation, and the bounds check passes */
+ if (tile->gid > 0 && tile->hasAnimation && tile->frameIndex < tile->animation.framesLength) {
+ tile->frameTime += dt;
+ /* If the current frame has been displayed for its whole duration, or longer */
+ if (tile->frameTime > tile->animation.frames[tile->frameIndex].duration) {
+ tile->frameTime -= tile->animation.frames[tile->frameIndex].duration;
+ /* Increment the frame index to display the next one... */
+ tile->frameIndex += 1;
+ /* ...unless the last frame was "last" in both senses */
+ if (tile->frameIndex == tile->animation.framesLength)
+ tile->frameIndex = 0; /* Wrap around to the first frame */
+ }
+ }
+ }
+}
+
+/**
+ * Helper function that creates a TmxObject equivalent to the given rectangle.
+ *
+ * @param rec A rectangle as raylib's Rectangle type.
+ * @return An equivalent TmxObject.
+ */
+TmxObject CreateRectangularTMXObject(Rectangle rec) {
+ TmxObject recAsObject;
+ memset(&recAsObject, 0, sizeof(TmxObject)); /* Zero initialize */
+
+ recAsObject.type = OBJECT_TYPE_RECTANGLE;
+ recAsObject.x = (double)rec.x;
+ recAsObject.y = (double)rec.y;
+ recAsObject.width = (double)rec.width;
+ recAsObject.height = (double)rec.height;
+ recAsObject.aabb = rec;
+
+ return recAsObject;
+}
+
+RAYTMX_DEC bool CheckCollisionTMXTileLayersRec(const TmxMap* map, const TmxLayer* layers, uint32_t layersLength,
+ Rectangle rec, TmxObject* outputObject) {
+ if (map == NULL || layers == NULL || layersLength == 0)
+ return false;
+
+ /* Check the rectangle against objects associated with tiles in the layers for collisions */
+ return CheckCollisionTMXTileLayerObject(map, layers, layersLength, CreateRectangularTMXObject(rec), outputObject);
+}
+
+/**
+ * Helper function that creates a TmxObject equivalent to the given circle.
+ *
+ * @param center The center point of the circle.
+ * @param radius The radius of the circle.
+ * @return An equivalent TmxObject.
+ */
+TmxObject CreateCircularTMXObject(Vector2 center, float radius) {
+ TmxObject circleAsObject;
+ memset(&circleAsObject, 0, sizeof(TmxObject)); /* Zero initialize */
+
+ circleAsObject.type = OBJECT_TYPE_ELLIPSE;
+ circleAsObject.x = (double)(center.x - radius);
+ circleAsObject.y = (double)(center.y - radius);
+ circleAsObject.width = 2.0 * (double)radius;
+ circleAsObject.height = 2.0 * (double)radius;
+ circleAsObject.aabb.x = center.x - radius;
+ circleAsObject.aabb.y = center.y - radius;
+ circleAsObject.aabb.width = 2.0f * radius;
+ circleAsObject.aabb.height = 2.0f * radius;
+
+ return circleAsObject;
+}
+
+RAYTMX_DEC bool CheckCollisionTMXTileLayersCircle(const TmxMap* map, const TmxLayer* layers, uint32_t layersLength,
+ Vector2 center, float radius, TmxObject* outputObject) {
+ if (map == NULL || layers == NULL || layersLength == 0)
+ return false;
+
+ /* Check the circle against objects associated with tiles in the layers for collisions */
+ return CheckCollisionTMXTileLayerObject(map, layers, layersLength, CreateCircularTMXObject(center, radius),
+ outputObject);
+}
+
+/**
+ * Helper function that creates a TmxObject equivalent to the given point.
+ *
+ * @param point The point.
+ * @return An equivalent TmxObject.
+ */
+TmxObject CreatePointTMXObject(Vector2 point) {
+ TmxObject pointAsObject;
+ memset(&pointAsObject, 0, sizeof(TmxObject)); /* Zero initialize */
+
+ pointAsObject.type = OBJECT_TYPE_POINT;
+ pointAsObject.x = (float)point.x;
+ pointAsObject.y = (float)point.y;
+ pointAsObject.aabb.x = (float)point.x;
+ pointAsObject.aabb.y = (float)point.y;
+
+ return pointAsObject;
+}
+
+RAYTMX_DEC bool CheckCollisionTMXTileLayersPoint(const TmxMap* map, const TmxLayer* layers, uint32_t layersLength,
+ Vector2 point, TmxObject* outputObject) {
+ if (map == NULL || layers == NULL || layersLength == 0)
+ return false;
+
+ /* Check the point against objects associated with tiles in the layers for collisions */
+ return CheckCollisionTMXTileLayerObject(map, layers, layersLength, CreatePointTMXObject(point), outputObject);
+}
+
+/**
+ * Helper function that creates a TmxObject equivalent to the given polygon with the given Axis-Aligned Bounding Box
+ * (AABB). If the AABB should be calculated from the given vertices, pass a rectangle with a zero or negative width or
+ * height.
+ *
+ * @param points An array of vertices defining the polygon. No repeats.
+ * @param pointCount The length of the array of vertices.
+ * @param aabb Bounding box of the polygon. Used for quicker, broad collision checks.
+ * @return An equivalent TmxObject.
+ */
+TmxObject CreatePolygonTMXObject(Vector2* points, int pointCount, Rectangle aabb) {
+ /* Create a polygon TMX object to represent the given polygon */
+ TmxObject polygonAsObject;
+ memset(&polygonAsObject, 0, sizeof(TmxObject)); /* Zero initialize */
+
+ polygonAsObject.type = OBJECT_TYPE_POLYGON;
+
+ if (aabb.width <= 0.0f || aabb.height <= 0.0f) { /* If the AABB wasn't pre-calculated or is just wrong */
+ /* Calculate the bounding box by searching for minimum and maximum coordinates of the vertices */
+ float minX = INFINITY, maxX = -INFINITY, minY = INFINITY, maxY = -INFINITY;
+ for (int i = 0; i < pointCount; i++) {
+ Vector2 point = points[i];
+ if (point.x < minX)
+ minX = point.x;
+ if (point.x > maxX)
+ maxX = point.x;
+ if (point.y < minY)
+ minY = point.y;
+ if (point.y > maxY)
+ maxY = point.y;
+ }
+ aabb.x = minX;
+ aabb.y = minY;
+ aabb.width = maxX - minX;
+ aabb.height = maxY - minY;
+ }
+
+ /* Polygons' points are relative to the object's position. Because this function takes points with absolute */
+ /* positions, we'll account for this by making the object's position zero. */
+ polygonAsObject.x = 0.0;
+ polygonAsObject.y = 0.0;
+ polygonAsObject.width = (double)aabb.width;
+ polygonAsObject.height = (double)aabb.height;
+ polygonAsObject.aabb = aabb;
+ polygonAsObject.points = points;
+ polygonAsObject.pointsLength = pointCount;
+
+ return polygonAsObject;
+}
+
+RAYTMX_DEC bool CheckCollisionTMXLayersPoly(const TmxMap* map, const TmxLayer* layers, uint32_t layersLength,
+ Vector2* points, int pointCount, TmxObject* outputObject) {
+ if (map == NULL || layers == NULL || layersLength == 0 || points == NULL || pointCount < 3)
+ return false;
+
+ /* Check the polygon against objects associated with tiles in the layers for collisions */
+ return CheckCollisionTMXTileLayerObject(map, layers, layersLength,
+ CreatePolygonTMXObject(points, pointCount, (Rectangle){0.0f}), outputObject);
+}
+
+RAYTMX_DEC bool CheckCollisionTMXLayersPolyEx(const TmxMap* map, const TmxLayer* layers, uint32_t layersLength,
+ Vector2* points, int pointCount, Rectangle aabb, TmxObject* outputObject) {
+ if (map == NULL || layers == NULL || layersLength == 0 || points == NULL || pointCount < 3)
+ return false;
+
+ /* Check the polygon against objects associated with tiles in the layers for collisions */
+ return CheckCollisionTMXTileLayerObject(map, layers, layersLength,
+ CreatePolygonTMXObject(points, pointCount, aabb), outputObject);
+}
+
+RAYTMX_DEC bool CheckCollisionTMXObjectGroupRec(TmxObjectGroup group, Rectangle rec, TmxObject* outputObject) {
+ if (group.objectsLength == 0 || rec.width < 0.0f || rec.height < 0.0f)
+ return false; /* Early-out opportunity. These cases would always return false. */
+
+ /* Check the rectangle against TMX objects in the group for collisions */
+ return CheckCollisionTMXObjectGroupObject(group, CreateRectangularTMXObject(rec), outputObject);
+}
+
+RAYTMX_DEC bool CheckCollisionTMXObjectGroupCircle(TmxObjectGroup group, Vector2 center, float radius,
+ TmxObject* outputObject) {
+ if (group.objectsLength == 0 || radius < 0.0f)
+ return false; /* Early-out opportunity. These cases would always return false. */
+
+ /* Check the circle against TMX objects in the group for collisions */
+ return CheckCollisionTMXObjectGroupObject(group, CreateCircularTMXObject(center, radius), outputObject);
+}
+
+RAYTMX_DEC bool CheckCollisionTMXObjectGroupPoint(TmxObjectGroup group, Vector2 point, TmxObject* outputObject) {
+ if (group.objectsLength == 0)
+ return false; /* Early-out opportunity. This case would always return false. */
+
+ /* Check the point against TMX objects in the group for collisions */
+ return CheckCollisionTMXObjectGroupObject(group, CreatePointTMXObject(point), outputObject);
+}
+
+RAYTMX_DEC bool CheckCollisionTMXObjectGroupPoly(TmxObjectGroup group, Vector2* points, int pointCount,
+ TmxObject* outputObject) {
+ if (group.objectsLength == 0 || points == NULL || pointCount < 3)
+ return false; /* Early-out opportunity. These cases would always return false. */
+
+ /* Check the polygon TMX object against other TMX objects in the group for collisions */
+ return CheckCollisionTMXObjectGroupObject(group, CreatePolygonTMXObject(points, pointCount, (Rectangle){0.0f}),
+ outputObject);
+}
+
+RAYTMX_DEC bool CheckCollisionTMXObjectGroupPolyEx(TmxObjectGroup group, Vector2* points, int pointCount,
+ Rectangle aabb, TmxObject* outputObject) {
+ if (group.objectsLength == 0 || points == NULL || pointCount < 3)
+ return false; /* Early-out opportunity. These cases would always return false. */
+
+ /* Check the polygon TMX object against other TMX objects in the group for collisions */
+ return CheckCollisionTMXObjectGroupObject(group, CreatePolygonTMXObject(points, pointCount, aabb), outputObject);
+}
+
+static LoadTextureCallback loadTextureOverride = NULL;
+
+RAYTMX_DEC void SetLoadTextureTMX(LoadTextureCallback callback) {
+ loadTextureOverride = callback;
+}
+
+static int tmxLogFlags = 0;
+
+RAYTMX_DEC void TraceLogTMX(int logLevel, const TmxMap* map) {
+ if (map == NULL)
+ return;
+
+ switch (map->orientation) {
+ case ORIENTATION_NONE: TraceLog(logLevel, "orientation: none"); break;
+ case ORIENTATION_ORTHOGONAL: TraceLog(logLevel, "orientation: orthogonal"); break;
+ case ORIENTATION_ISOMETRIC: TraceLog(logLevel, "orientation: isometric"); break;
+ case ORIENTATION_STAGGERED: TraceLog(logLevel, "orientation: staggered"); break;
+ case ORIENTATION_HEXAGONAL: TraceLog(logLevel, "orientation: hexagonal"); break;
+ }
+
+ switch (map->renderOrder) {
+ case RENDER_ORDER_RIGHT_DOWN: TraceLog(logLevel, "render order: right-down"); break;
+ case RENDER_ORDER_RIGHT_UP: TraceLog(logLevel, "render order: right-up"); break;
+ case RENDER_ORDER_LEFT_DOWN: TraceLog(logLevel, "render order: left-down"); break;
+ case RENDER_ORDER_LEFT_UP: TraceLog(logLevel, "render order: left-up"); break;
+ }
+
+ TraceLog(logLevel, "width: %u tiles", map->width);
+ TraceLog(logLevel, "height: %u tiles", map->height);
+ TraceLog(logLevel, "tile width: %u pixels", map->tileWidth);
+ TraceLog(logLevel, "tile height: %u pixels", map->tileHeight);
+ TraceLog(logLevel, "parallax origin X: %d pixels", map->parallaxOriginX);
+ TraceLog(logLevel, "parallax origin Y: %d pixels", map->parallaxOriginY);
+ if (map->hasBackgroundColor)
+ TraceLog(logLevel, "background color: 0x%08X", map->backgroundColor);
+
+ TraceLogTMXTilesets(logLevel, map->orientation, map->tilesets, map->tilesetsLength, 0);
+ TraceLogTMXProperties(logLevel, map->properties, map->propertiesLength, 0);
+ TraceLogTMXLayers(logLevel, map->layers, map->layersLength, 0);
+}
+
+RAYTMX_DEC void SetTraceLogFlagsTMX(int logFlags) {
+ tmxLogFlags = logFlags;
+}
+
+/**********************************************************************************************************************/
+/* Private implementation. */
+
+RaytmxExternalTileset LoadTSX(const char* fileName) {
+ RaytmxState raytmxState[1];
+ memset(raytmxState, 0, sizeof(RaytmxState)); /* Initialize all values to zero, NULL, or an equivalent enum value */
+ raytmxState->format = FORMAT_TSX;
+
+ /* Initialize an external tileset object */
+ RaytmxExternalTileset externalTileset;
+ memset(&externalTileset, 0, sizeof(RaytmxExternalTileset));
+
+ /* Do format-agnostic parsing of the document. The state object will be populated with raytmx's models of the */
+ /* equivalent TMX, TSX, and/or TX elements. */
+ ParseDocument(raytmxState, fileName);
+ if (!raytmxState->isSuccess)
+ return externalTileset; /* Will have 'isSuccess' set to false to indicate a failure */
+
+ if (raytmxState->tilesetsRoot != NULL) { /* If there is at least one tileset */
+ /* Copy the root tileset so it can be returned */
+ externalTileset.tileset = raytmxState->tilesetsRoot->tileset;
+ externalTileset.isSuccess = true;
+ /* TSX files should have only one tileset so any others will be freed/unloaded immediately */
+ RaytmxTilesetNode* tilesetIterator = raytmxState->tilesetsRoot->next;
+ while (tilesetIterator != NULL) {
+ FreeTileset(tilesetIterator->tileset);
+ tilesetIterator = tilesetIterator->next;
+ }
+ } else
+ TraceLog(LOG_WARNING, "RAYTMX: TSX file (external tileset) \"%s\" does not contain any tilesets", fileName);
+
+ /* Free the linked lists and zeroize related values */
+ FreeState(raytmxState);
+
+ return externalTileset;
+}
+
+RaytmxObjectTemplate LoadTX(const char* fileName) {
+ RaytmxState raytmxState[1];
+ memset(raytmxState, 0, sizeof(RaytmxState)); /* Initialize all values to zero, NULL, or an equivalent enum value */
+ raytmxState->format = FORMAT_TX;
+
+ /* Initialize an object template object */
+ RaytmxObjectTemplate objectTemplate;
+ memset(&objectTemplate, 0, sizeof(RaytmxObjectTemplate));
+
+ /* Do format-agnostic parsing of the document. The state object will be populated with raytmx's models of the */
+ /* equivalent TMX, TSX, and/or TX elements. */
+ ParseDocument(raytmxState, fileName);
+ if (!raytmxState->isSuccess)
+ return objectTemplate; /* Will have 'isSuccess' set to false to indicate a failure */
+
+ if (raytmxState->objectsRoot != NULL) { /* If there is at least one object */
+ /* Copy the root object so it can be returned */
+ objectTemplate.object = raytmxState->objectsRoot->object;
+ objectTemplate.isSuccess = true;
+ /* TX files should have only one object so any others will be freed/unloaded immediately */
+ RaytmxObjectNode* objectIterator = raytmxState->objectsRoot->next;
+ while (objectIterator != NULL) {
+ FreeObject(objectIterator->object);
+ objectIterator = objectIterator->next;
+ }
+ } else
+ TraceLog(LOG_WARNING, "RAYTMX: TX file (object template) \"%s\" does not contain any objects", fileName);
+
+ if (raytmxState->tilesetsRoot != NULL) { /* If there is at least one tileset */
+ /* Object templates may have a tileset. This is for cases where the object references a tile (i.e. its 'gid' */
+ /* attribute is set). Copy the root tileset so it can be returned */
+ objectTemplate.tileset = raytmxState->tilesetsRoot->tileset;
+ objectTemplate.hasTileset = true;
+ /* TX files should have at most one tileset so any others will be freed/unloaded immediately */
+ RaytmxTilesetNode* tilesetsIterator = raytmxState->tilesetsRoot->next;
+ while (tilesetsIterator != NULL) {
+ FreeTileset(tilesetsIterator->tileset);
+ tilesetsIterator = tilesetsIterator->next;
+ }
+ }
+
+ /* Free the linked lists and zeroize related values */
+ FreeState(raytmxState);
+
+ return objectTemplate;
+}
+
+void ParseDocument(RaytmxState* raytmxState, const char* fileName) {
+ char* content = LoadFileText(fileName);
+ if (content == NULL) {
+ TraceLog(LOG_ERROR, "RAYTMX: Failed to open \"%s\"", fileName);
+ return;
+ }
+ size_t contentLength = strlen(content);
+
+ StringCopy(raytmxState->documentDirectory, GetDirectoryPath2(fileName));
+
+ hoxml_context_t hoxmlContext[1];
+ size_t bufferLength = contentLength;
+ char* buffer = (char*)MemAlloc((unsigned int)bufferLength);
+ hoxml_init(hoxmlContext, buffer, bufferLength);
+
+ hoxml_code_t code;
+ while ((code = hoxml_parse(hoxmlContext, content, contentLength)) != HOXML_END_OF_DOCUMENT) {
+ if (code > HOXML_END_OF_DOCUMENT) { /* If there's information about an element, attribute, whatever */
+ switch (code) {
+ case HOXML_ELEMENT_BEGIN: HandleElementBegin(raytmxState, hoxmlContext); break;
+ case HOXML_ELEMENT_END: HandleElementEnd(raytmxState, hoxmlContext); break;
+ case HOXML_ATTRIBUTE: HandleAttribute(raytmxState, hoxmlContext);
+ case HOXML_PROCESSING_INSTRUCTION_BEGIN: break;
+ case HOXML_PROCESSING_INSTRUCTION_END: break;
+ default: break; /* No other cases to handle but compilers like to complain */
+ }
+ } else if (code < HOXML_END_OF_DOCUMENT) { /* If there was an error, recoverable or not */
+ switch (code) {
+ case HOXML_ERROR_INSUFFICIENT_MEMORY: {
+ /* This is one we can recover from by expanding the buffer. In this case, it will be doubled. */
+ TraceLog(LOG_DEBUG, "RAYTMX: Allocating a new XML parsing buffer due to insufficient memory");
+ bufferLength *= 2;
+ char* newBuffer = (char*)MemAlloc((unsigned int)bufferLength);
+ hoxml_realloc(hoxmlContext, newBuffer, bufferLength);
+ MemFree(buffer);
+ buffer = newBuffer;
+ continue;
+ } case HOXML_ERROR_UNEXPECTED_EOF:
+ TraceLog(LOG_ERROR, "RAYTMX: Unexpected end of file");
+ break;
+ case HOXML_ERROR_SYNTAX:
+ TraceLog(LOG_ERROR, "RAYTMX: Invalid syntax: line %d, column %d", hoxmlContext->line,
+ hoxmlContext->column);
+ break;
+ case HOXML_ERROR_ENCODING:
+ TraceLog(LOG_ERROR, "RAYTMX: Character encoding error: line %d, column %d", hoxmlContext->line,
+ hoxmlContext->column);
+ break;
+ case HOXML_ERROR_TAG_MISMATCH:
+ TraceLog(LOG_ERROR, "RAYTMX: Close tag does not match open tag: line %d, column %d",
+ hoxmlContext->line, hoxmlContext->column);
+ break;
+ case HOXML_ERROR_INVALID_DOCUMENT_TYPE_DECLARATION:
+ case HOXML_ERROR_INVALID_DOCUMENT_DECLARATION:
+ TraceLog(LOG_ERROR, "RAYTMX: Document (type) declaration error: line %d, column %d",
+ hoxmlContext->line, hoxmlContext->column);
+ break;
+ default: break; /* Keep the compiler happy */
+ }
+ UnloadFileText(content);
+ return;
+ }
+ }
+
+ UnloadFileText(content);
+ MemFree(buffer);
+ raytmxState->isSuccess = true;
+}
+
+void HandleElementBegin(RaytmxState* raytmxState, hoxml_context_t* hoxmlContext) {
+ if (raytmxState == NULL || hoxmlContext == NULL)
+ return;
+
+ if (strcmp(hoxmlContext->tag, "map") == 0)
+ ;
+ else if (strcmp(hoxmlContext->tag, "properties") == 0) {
+ /* TMX allows nested properties but they are not (currently?) supported. To avoid memory leaks <properties> */
+ /* depth is tracked. */
+ raytmxState->propertiesDepth += 1;
+ } else if (strcmp(hoxmlContext->tag, "property") == 0)
+ raytmxState->property = AddProperty(raytmxState);
+ else if (strcmp(hoxmlContext->tag, "tileset") == 0)
+ raytmxState->tileset = AddTileset(raytmxState);
+ else if (strcmp(hoxmlContext->tag, "image") == 0) {
+ /* If any of the elements that may have an image is/are open */
+ if (raytmxState->tilesetTile != NULL || raytmxState->tileset != NULL || raytmxState->imageLayer != NULL) {
+ /* If the open element already has an image */
+ /* Note: In all cases, the element can contain at most one <image> */
+ if ((raytmxState->tilesetTile != NULL && raytmxState->tilesetTile->hasImage) ||
+ (raytmxState->tileset != NULL && raytmxState->tileset->hasImage) ||
+ (raytmxState->imageLayer != NULL && raytmxState->imageLayer->hasImage)) {
+ TraceLog(LOG_WARNING, "RAYTMX: an element contained multiple images; the image on line %d will be "
+ "dropped", hoxmlContext->line);
+ } else {
+ /* <image> elements can be children of <tileset>, <tile>, or <imagelayer> elements */
+ if (raytmxState->tilesetTile != NULL) {
+ raytmxState->tilesetTile->hasImage = true;
+ raytmxState->image = &raytmxState->tilesetTile->image;
+ } else if (raytmxState->tileset != NULL) {
+ raytmxState->tileset->hasImage = true;
+ raytmxState->image = &raytmxState->tileset->image;
+ } else if (raytmxState->imageLayer != NULL) {
+ raytmxState->imageLayer->hasImage = true;
+ raytmxState->image = &raytmxState->imageLayer->image;
+ }
+ }
+ }
+ } /* strcmp(hoxmlContext->tag, "image") == 0 */
+ else if (strcmp(hoxmlContext->tag, "tile") == 0) {
+ /* <tile> elements can be children of <tileset> or <layer>. They are also not the same element in that they */
+ /* have entirely different attributes and a tileset's <tile> may have children. */
+ if (raytmxState->tileset != NULL)
+ raytmxState->tilesetTile = AddTilesetTile(raytmxState);
+ /* Layer <tile>s are added during attribute handling because they provide a GID attribute and nothing else */
+ } /* strcmp(hoxmlContext->tag, "tile") == 0 */
+ else if (strcmp(hoxmlContext->tag, "animation") == 0) {
+ if (raytmxState->tilesetTile != NULL)
+ raytmxState->tilesetTile->hasAnimation = true;
+ } else if (strcmp(hoxmlContext->tag, "frame") == 0)
+ raytmxState->animationFrame = AddAnimationFrame(raytmxState);
+ else if (strcmp(hoxmlContext->tag, "layer") == 0) {
+ /* Allocate a new layer with 'tileLayer' allocated and append it to the current group, if it exists */
+ raytmxState->layer = AddGenericLayer(raytmxState, /* isGroup: */ false);
+ raytmxState->layer->type = LAYER_TYPE_TILE_LAYER;
+ raytmxState->tileLayer = &raytmxState->layer->exact.tileLayer;
+ } else if (strcmp(hoxmlContext->tag, "objectgroup") == 0) {
+ if (raytmxState->tilesetTile != NULL) { /* If the object group is a child of a <tile>, it's collision info */
+ raytmxState->objectGroup = &raytmxState->tilesetTile->objectGroup;
+ /* Child objects (rectangles, points, ellipses, or polygons) are expected to follow */
+ } else {
+ /* Allocate a new layer with 'objectGroup' allocated and append it to the current group, if it exists */
+ raytmxState->layer = AddGenericLayer(raytmxState, /* isGroup: */ false);
+ raytmxState->layer->type = LAYER_TYPE_OBJECT_GROUP;
+ raytmxState->objectGroup = &raytmxState->layer->exact.objectGroup;
+ }
+ } else if (strcmp(hoxmlContext->tag, "object") == 0) {
+ /* <object> elements are typically only allowable as children of <objectgroup>s but object templates, TX */
+ /* files, contain them as children of root <template> */
+ if (raytmxState->objectGroup != NULL || raytmxState->format == FORMAT_TX)
+ raytmxState->object = AddObject(raytmxState);
+ } else if (strcmp(hoxmlContext->tag, "ellipse") == 0) {
+ if (raytmxState->object != NULL) {
+ /* An <ellipse> within an <object> indicates its type but the <object>'s 'x,' 'y,' 'width,' and 'height' */
+ /* attributes are used to define the ellipse so assigning the type is all that's necessary */
+ raytmxState->object->type = OBJECT_TYPE_ELLIPSE;
+ }
+ } else if (strcmp(hoxmlContext->tag, "point") == 0) {
+ if (raytmxState->object != NULL) {
+ /* A <point> within an <object> indicates its type but the <object>'s 'x' and 'y' attributes are used to */
+ /* define the point so assigning the type is all that's necessary */
+ raytmxState->object->type = OBJECT_TYPE_POINT;
+ }
+ } else if (strcmp(hoxmlContext->tag, "polygon") == 0) {
+ if (raytmxState->object != NULL) {
+ /* Note: <polygon>s and <polyline>s have a list of points/vertices defined in a 'points' attribute */
+ raytmxState->object->type = OBJECT_TYPE_POLYGON;
+ }
+ } else if (strcmp(hoxmlContext->tag, "polyline") == 0) {
+ if (raytmxState->object != NULL) {
+ /* Note: <polyline>s and <polygone>s have a list of points/vertices defined in a 'points' attribute */
+ raytmxState->object->type = OBJECT_TYPE_POLYLINE;
+ }
+ } else if (strcmp(hoxmlContext->tag, "text") == 0) {
+ if (raytmxState->object != NULL) {
+ raytmxState->object->type = OBJECT_TYPE_TEXT;
+ raytmxState->object->text = (TmxText*)MemAllocZero(sizeof(TmxText));
+ /* There are a couple non-zero default values for <text> attributes: */
+ raytmxState->object->text->pixelSize = 16;
+ raytmxState->object->text->color.a = 255; /* Full opacity black */
+ raytmxState->object->text->kerning = 1;
+ /* The font family will also default to "sans-serif" when the element ends if there is no attribute */
+ }
+ } else if (strcmp(hoxmlContext->tag, "imagelayer") == 0) {
+ /* Allocate a new layer with 'imageLayer' allocated and append it to the current group, if it exists */
+ raytmxState->layer = AddGenericLayer(raytmxState, /* isGroup: */ false);
+ raytmxState->layer->type = LAYER_TYPE_IMAGE_LAYER;
+ raytmxState->imageLayer = &raytmxState->layer->exact.imageLayer;
+ } else if (strcmp(hoxmlContext->tag, "group") == 0) {
+ /* Allocate a new layer and append it to the current group, if it exists */
+ raytmxState->layer = AddGenericLayer(raytmxState, /* isGroup: */ true);
+ raytmxState->layer->type = LAYER_TYPE_GROUP;
+ }
+}
+
+void HandleAttribute(RaytmxState* raytmxState, hoxml_context_t* hoxmlContext) {
+ if (raytmxState == NULL || hoxmlContext == NULL)
+ return;
+
+ if (strcmp(hoxmlContext->tag, "map") == 0) {
+ if (strcmp(hoxmlContext->attribute, "orientation") == 0) {
+ if (strcmp(hoxmlContext->value, "orthogonal") == 0)
+ raytmxState->mapOrientation = ORIENTATION_ORTHOGONAL;
+ else if (strcmp(hoxmlContext->value, "isometric") == 0)
+ raytmxState->mapOrientation = ORIENTATION_ISOMETRIC;
+ else if (strcmp(hoxmlContext->value, "staggered") == 0)
+ raytmxState->mapOrientation = ORIENTATION_STAGGERED;
+ else if (strcmp(hoxmlContext->value, "hexagonal") == 0)
+ raytmxState->mapOrientation = ORIENTATION_HEXAGONAL;
+ } /* strcmp(hoxmlContext->attribute, "orientation") == 0 */
+ else if (strcmp(hoxmlContext->attribute, "renderorder") == 0) {
+ if (strcmp(hoxmlContext->value, "right-down") == 0)
+ raytmxState->mapRenderOrder = RENDER_ORDER_RIGHT_DOWN;
+ else if (strcmp(hoxmlContext->value, "right-up") == 0)
+ raytmxState->mapRenderOrder = RENDER_ORDER_RIGHT_UP;
+ else if (strcmp(hoxmlContext->value, "left-down") == 0)
+ raytmxState->mapRenderOrder = RENDER_ORDER_LEFT_DOWN;
+ else if (strcmp(hoxmlContext->value, "left-up") == 0)
+ raytmxState->mapRenderOrder = RENDER_ORDER_LEFT_UP;
+ } /* strcmp(hoxmlContext->attribute, "renderorder") == 0 */
+ else if (strcmp(hoxmlContext->attribute, "width") == 0)
+ raytmxState->mapWidth = atoi(hoxmlContext->value);
+ else if (strcmp(hoxmlContext->attribute, "height") == 0)
+ raytmxState->mapHeight = atoi(hoxmlContext->value);
+ else if (strcmp(hoxmlContext->attribute, "tilewidth") == 0)
+ raytmxState->mapTileWidth = atoi(hoxmlContext->value);
+ else if (strcmp(hoxmlContext->attribute, "tileheight") == 0)
+ raytmxState->mapTileHeight = atoi(hoxmlContext->value);
+ else if (strcmp(hoxmlContext->attribute, "parallaxoriginx") == 0)
+ raytmxState->mapParallaxOriginX = atoi(hoxmlContext->value);
+ else if (strcmp(hoxmlContext->attribute, "parallaxoriginy") == 0)
+ raytmxState->mapParallaxOriginY = atoi(hoxmlContext->value);
+ else if (strcmp(hoxmlContext->attribute, "backgroundcolor") == 0) {
+ raytmxState->mapBackgroundColor = GetColorFromHexString(hoxmlContext->value);
+ raytmxState->mapHasBackgroundColor = true;
+ }
+ } /* strcmp(hoxmlContext->tag, "map") == 0 */
+ else if (strcmp(hoxmlContext->tag, "property") == 0) {
+ if (raytmxState->property != NULL) {
+ if (strcmp(hoxmlContext->attribute, "name") == 0) {
+ raytmxState->property->name = (char*)MemAllocZero((unsigned int)strlen(hoxmlContext->value) + 1);
+ StringCopy(raytmxState->property->name, hoxmlContext->value);
+ } else if (strcmp(hoxmlContext->attribute, "type") == 0) {
+ if (strcmp(hoxmlContext->value, "string") == 0)
+ raytmxState->property->type = PROPERTY_TYPE_STRING;
+ else if (strcmp(hoxmlContext->value, "int") == 0)
+ raytmxState->property->type = PROPERTY_TYPE_INT;
+ else if (strcmp(hoxmlContext->value, "float") == 0)
+ raytmxState->property->type = PROPERTY_TYPE_FLOAT;
+ else if (strcmp(hoxmlContext->value, "bool") == 0)
+ raytmxState->property->type = PROPERTY_TYPE_BOOL;
+ else if (strcmp(hoxmlContext->value, "color") == 0)
+ raytmxState->property->type = PROPERTY_TYPE_COLOR;
+ else if (strcmp(hoxmlContext->value, "file") == 0)
+ raytmxState->property->type = PROPERTY_TYPE_FILE;
+ else if (strcmp(hoxmlContext->value, "object") == 0)
+ raytmxState->property->type = PROPERTY_TYPE_OBJECT;
+ /* TMX documentation also mentions a "class" type but doesn't describe what it is nor does Tiled list */
+ /* it as an option when adding a property. So what is it? Unsupported, that's what. */
+ } /* strcmp(hoxmlContext->attribute, "type") == 0 */
+ else if (strcmp(hoxmlContext->attribute, "value") == 0) {
+ /* Although unlikley, it's possible that 'value' attribute will be parsed before the 'type' */
+ /* attribute. In that case, doing a cast/conversion now may not be possible. To avoid this, the raw */
+ /* string value is copied to 'stringValue' temporarily, or permanently for string and file types, and */
+ /* the cast/conversion will happen at the end of the element if needed. */
+ raytmxState->property->stringValue = (char*)MemAlloc((unsigned int)strlen(hoxmlContext->value) + 1);
+ StringCopy(raytmxState->property->stringValue, hoxmlContext->value);
+ } /* strcmp(hoxmlContext->attribute, "value") == 0 */
+ } /* raytmxState->property != NULL */
+ } /* strcmp(hoxmlContext->tag, "property") == 0 */
+ else if (strcmp(hoxmlContext->tag, "tileset") == 0) {
+ if (raytmxState->tileset != NULL) {
+ if (strcmp(hoxmlContext->attribute, "firstgid") == 0)
+ raytmxState->tileset->firstGid = atoi(hoxmlContext->value);
+ else if (strcmp(hoxmlContext->attribute, "source") == 0) {
+ raytmxState->tileset->source = (char*)MemAlloc((unsigned int)strlen(hoxmlContext->value) + 1);
+ StringCopy(raytmxState->tileset->source, hoxmlContext->value);
+ /* 'source' points to an external TSX file that defines the majority of the tileset. Try to load it. */
+ RaytmxExternalTileset externalTileset = LoadTSX(JoinPath(raytmxState->documentDirectory,
+ hoxmlContext->value));
+ if (externalTileset.isSuccess) {
+ /* A <tileset> within a <map> will have two attributes: 'firstgid' and 'source.' The rest of */
+ /* the tileset's details are in the external TSX that 'source' points to. They need to be merged. */
+ /* Remember the two internal attributes. */
+ uint32_t tempFirstGid = raytmxState->tileset->firstGid;
+ char* tempSource = raytmxState->tileset->source;
+ /* Assign all values from the TSX's tileset to the one within the state object. This will */
+ /* overrite the values of 'firstGid' and 'source.' */
+ *raytmxState->tileset = externalTileset.tileset;
+ /* Reassign the original 'firstGid' and 'source' values */
+ raytmxState->tileset->firstGid = tempFirstGid;
+ raytmxState->tileset->source = tempSource;
+ }
+ } else if (strcmp(hoxmlContext->attribute, "name") == 0) {
+ raytmxState->tileset->name = (char*)MemAllocZero((unsigned int)strlen(hoxmlContext->value) + 1);
+ StringCopy(raytmxState->tileset->name, hoxmlContext->value);
+ } else if (strcmp(hoxmlContext->attribute, "class") == 0) {
+ raytmxState->tileset->classString = (char*)MemAllocZero((unsigned int)strlen(hoxmlContext->value) + 1);
+ StringCopy(raytmxState->tileset->classString, hoxmlContext->value);
+ } else if (strcmp(hoxmlContext->attribute, "tilewidth") == 0)
+ raytmxState->tileset->tileWidth = atoi(hoxmlContext->value);
+ else if (strcmp(hoxmlContext->attribute, "tileheight") == 0)
+ raytmxState->tileset->tileHeight = atoi(hoxmlContext->value);
+ else if (strcmp(hoxmlContext->attribute, "spacing") == 0)
+ raytmxState->tileset->spacing = atoi(hoxmlContext->value);
+ else if (strcmp(hoxmlContext->attribute, "margin") == 0)
+ raytmxState->tileset->margin = atoi(hoxmlContext->value);
+ else if (strcmp(hoxmlContext->attribute, "tilecount") == 0)
+ raytmxState->tileset->tileCount = atoi(hoxmlContext->value);
+ else if (strcmp(hoxmlContext->attribute, "columns") == 0)
+ raytmxState->tileset->columns = atoi(hoxmlContext->value);
+ else if (strcmp(hoxmlContext->attribute, "objectalignment") == 0) {
+ if (strcmp(hoxmlContext->value, "unspecified") == 0)
+ raytmxState->tileset->objectAlignment = OBJECT_ALIGNMENT_UNSPECIFIED;
+ else if (strcmp(hoxmlContext->value, "topleft") == 0)
+ raytmxState->tileset->objectAlignment = OBJECT_ALIGNMENT_TOP_LEFT;
+ else if (strcmp(hoxmlContext->value, "top") == 0)
+ raytmxState->tileset->objectAlignment = OBJECT_ALIGNMENT_TOP;
+ else if (strcmp(hoxmlContext->value, "topright") == 0)
+ raytmxState->tileset->objectAlignment = OBJECT_ALIGNMENT_TOP_RIGHT;
+ else if (strcmp(hoxmlContext->value, "left") == 0)
+ raytmxState->tileset->objectAlignment = OBJECT_ALIGNMENT_LEFT;
+ else if (strcmp(hoxmlContext->value, "center") == 0)
+ raytmxState->tileset->objectAlignment = OBJECT_ALIGNMENT_CENTER;
+ else if (strcmp(hoxmlContext->value, "right") == 0)
+ raytmxState->tileset->objectAlignment = OBJECT_ALIGNMENT_RIGHT;
+ else if (strcmp(hoxmlContext->value, "bottomleft") == 0)
+ raytmxState->tileset->objectAlignment = OBJECT_ALIGNMENT_BOTTOM_LEFT;
+ else if (strcmp(hoxmlContext->value, "bottom") == 0)
+ raytmxState->tileset->objectAlignment = OBJECT_ALIGNMENT_BOTTOM;
+ else if (strcmp(hoxmlContext->value, "bottomright") == 0)
+ raytmxState->tileset->objectAlignment = OBJECT_ALIGNMENT_BOTTOM_RIGHT;
+ }
+ } /* raytmState->tileset != NULL */
+ } /* strcmp(hoxmlContext->tag, "tileset") == 0 */
+ else if (strcmp(hoxmlContext->tag, "tileoffset") == 0) {
+ if (raytmxState->tileset != NULL) {
+ if (strcmp(hoxmlContext->attribute, "x") == 0)
+ raytmxState->tileset->tileOffsetX = atoi(hoxmlContext->value);
+ else if (strcmp(hoxmlContext->attribute, "y") == 0)
+ raytmxState->tileset->tileOffsetY = atoi(hoxmlContext->value);
+ }
+ } /* strcmp(hoxmlContext->tag, "tileoffset") == 0 */
+ else if (strcmp(hoxmlContext->tag, "image") == 0) {
+ if (raytmxState->image != NULL) {
+ if (strcmp(hoxmlContext->attribute, "source") == 0) {
+ raytmxState->image->source = (char*)MemAllocZero((unsigned int)strlen(hoxmlContext->value) + 1);
+ StringCopy(raytmxState->image->source, hoxmlContext->value);
+ RaytmxCachedTextureNode* cachedTexture = LoadCachedTexture(raytmxState, hoxmlContext->value);
+ if (cachedTexture != NULL)
+ raytmxState->image->texture = cachedTexture->texture;
+ } else if (strcmp(hoxmlContext->attribute, "trans") == 0) {
+ raytmxState->image->trans = GetColorFromHexString(hoxmlContext->value);
+ raytmxState->image->hasTrans = true;
+ } else if (strcmp(hoxmlContext->attribute, "width") == 0)
+ raytmxState->image->width = atoi(hoxmlContext->value);
+ else if (strcmp(hoxmlContext->attribute, "height") == 0)
+ raytmxState->image->height = atoi(hoxmlContext->value);
+ }
+ } /* strcmp(hoxmlContext->tag, "image") == 0 */
+ else if (strcmp(hoxmlContext->tag, "tile") == 0) {
+ if (raytmxState->tilesetTile != NULL) { /* If the <tile> corresponds to a tileset tile */
+ if (strcmp(hoxmlContext->attribute, "id") == 0)
+ raytmxState->tilesetTile->id = atoi(hoxmlContext->value);
+ else if (strcmp(hoxmlContext->attribute, "type") == 0 || strcmp(hoxmlContext->attribute, "class") == 0) {
+ raytmxState->tilesetTile->classString =
+ (char*)MemAllocZero((unsigned int)strlen(hoxmlContext->value) + 1);
+ StringCopy(raytmxState->tilesetTile->classString, hoxmlContext->value);
+ } else if (strcmp(hoxmlContext->attribute, "x") == 0)
+ raytmxState->tilesetTile->x = atoi(hoxmlContext->value);
+ else if (strcmp(hoxmlContext->attribute, "y") == 0)
+ raytmxState->tilesetTile->y = atoi(hoxmlContext->value);
+ else if (strcmp(hoxmlContext->attribute, "width") == 0)
+ raytmxState->tilesetTile->width = atoi(hoxmlContext->value);
+ else if (strcmp(hoxmlContext->attribute, "height") == 0)
+ raytmxState->tilesetTile->height = atoi(hoxmlContext->value);
+ } else { /* If the <tile> corresponds to a layer tile */
+ if (strcmp(hoxmlContext->attribute, "gid") == 0)
+ AddTileLayerTile(raytmxState, atoi(hoxmlContext->value));
+ }
+ } /* strcmp(hoxmlContext->tag, "tile") == 0 */
+ else if (strcmp(hoxmlContext->tag, "frame") == 0) {
+ if (raytmxState->animationFrame != NULL) {
+ if (strcmp(hoxmlContext->attribute, "tileid") == 0)
+ raytmxState->animationFrame->gid = atoi(hoxmlContext->value);
+ else if (strcmp(hoxmlContext->attribute, "duration") == 0)
+ raytmxState->animationFrame->duration = (float)atoi(hoxmlContext->value) / 1000.0f;
+ }
+ } /* strcmp(hoxmlContext->tag, "frame") == 0 */
+ else if (strcmp(hoxmlContext->tag, "layer") == 0) {
+ if (raytmxState->tileLayer != NULL) {
+ /* Check for attributes specific to <layer> layers */
+ if (strcmp(hoxmlContext->attribute, "width") == 0)
+ raytmxState->tileLayer->width = atoi(hoxmlContext->value);
+ else if (strcmp(hoxmlContext->attribute, "height") == 0)
+ raytmxState->tileLayer->height = atoi(hoxmlContext->value);
+ }
+ } /* strcmp(hoxmlContext->tag, "layer") == 0) */
+ else if (strcmp(hoxmlContext->tag, "data") == 0) {
+ if (raytmxState->tileLayer != NULL) { /* If this <data> applies to a <layer> */
+ if (strcmp(hoxmlContext->attribute, "encoding") == 0) {
+ raytmxState->tileLayer->encoding = (char*)MemAllocZero((unsigned int)strlen(hoxmlContext->value) + 1);
+ StringCopy(raytmxState->tileLayer->encoding, hoxmlContext->value);
+ } else if (strcmp(hoxmlContext->attribute, "compression") == 0) {
+ raytmxState->tileLayer->compression =
+ (char*)MemAllocZero((unsigned int)strlen(hoxmlContext->value) + 1);
+ StringCopy(raytmxState->tileLayer->compression, hoxmlContext->value);
+ }
+ } else if (raytmxState->image != NULL) { /* If this <data> applies to an <image> */
+ /* TODO (?): The TMX map format documentation says an <image> can contain a <data> element but doesn't */
+ /* provide any more information than that. Tiled doesn't seem to have a feature for this either. */
+ }
+ } /* strcmp(hoxmlContext->tag, "data") == 0 */
+ else if (strcmp(hoxmlContext->tag, "objectgroup") == 0) {
+ if (raytmxState->objectGroup != NULL) {
+ /* Check for attributes specific to <objectgroup> layers */
+ if (strcmp(hoxmlContext->attribute, "color") == 0) {
+ raytmxState->objectGroup->color = GetColorFromHexString(hoxmlContext->value);
+ raytmxState->objectGroup->hasColor = true;
+ } /* else if (strcmp(hoxmlContext->attribute, "width") == 0)
+ raytmxState->objectGroup->width = atoi(hoxmlContext->value); */ /* "Meaningless" according to docs. */
+ /* else if (strcmp(hoxmlContext->attribute, "height") == 0)
+ raytmxState->objectGroup->height = atoi(hoxmlContext->value); */ /* "Meaningless" according to docs. */
+ else if (strcmp(hoxmlContext->attribute, "draworder") == 0) {
+ if (strcmp(hoxmlContext->value, "index") == 0)
+ raytmxState->objectGroup->drawOrder = OBJECT_GROUP_DRAW_ORDER_INDEX;
+ else if (strcmp(hoxmlContext->value, "topdown") == 0)
+ raytmxState->objectGroup->drawOrder = OBJECT_GROUP_DRAW_ORDER_TOP_DOWN;
+ }
+ }
+ } /* strcmp(hoxmlContext->tag, "objectgroup") == 0 */
+ else if (strcmp(hoxmlContext->tag, "object") == 0) {
+ if (raytmxState->object != NULL) {
+ if (strcmp(hoxmlContext->attribute, "id") == 0)
+ raytmxState->object->id = atoi(hoxmlContext->value);
+ else if (strcmp(hoxmlContext->attribute, "name") == 0) {
+ raytmxState->object->name = (char*)MemAllocZero((unsigned int)strlen(hoxmlContext->value) + 1);
+ StringCopy(raytmxState->object->name, hoxmlContext->value);
+ } else if (strcmp(hoxmlContext->attribute, "type") == 0) {
+ raytmxState->object->typeString = (char*)MemAllocZero((unsigned int)strlen(hoxmlContext->value) + 1);
+ StringCopy(raytmxState->object->typeString, hoxmlContext->value);
+ } else if (strcmp(hoxmlContext->attribute, "x") == 0)
+ raytmxState->object->x = atof(hoxmlContext->value);
+ else if (strcmp(hoxmlContext->attribute, "y") == 0)
+ raytmxState->object->y = atof(hoxmlContext->value);
+ else if (strcmp(hoxmlContext->attribute, "width") == 0)
+ raytmxState->object->width = atof(hoxmlContext->value);
+ else if (strcmp(hoxmlContext->attribute, "height") == 0)
+ raytmxState->object->height = atof(hoxmlContext->value);
+ else if (strcmp(hoxmlContext->attribute, "rotation") == 0)
+ raytmxState->object->rotation = atof(hoxmlContext->value);
+ else if (strcmp(hoxmlContext->attribute, "gid") == 0) {
+ raytmxState->object->gid = atoi(hoxmlContext->value);
+ /* The presence of a 'gid' attribute also indicates the object's type is that of a tile */
+ raytmxState->object->type = OBJECT_TYPE_TILE;
+ } else if (strcmp(hoxmlContext->attribute, "visible") == 0)
+ raytmxState->object->visible = atoi(hoxmlContext->value) != 0 ? true : false;
+ else if (strcmp(hoxmlContext->attribute, "template") == 0) {
+ raytmxState->object->templateString =
+ (char*)MemAllocZero((unsigned int)strlen(hoxmlContext->value) + 1);
+ StringCopy(raytmxState->object->templateString, hoxmlContext->value);
+ }
+ }
+ } /* strcmp(hoxmlContext->tag, "object") == 0 */
+ else if (strcmp(hoxmlContext->tag, "polygon") == 0 || strcmp(hoxmlContext->tag, "polyline") == 0) {
+ /* <polygon> and <polyline>, children of <object>, both have just one attribute: 'points' */
+ if (raytmxState->object != NULL && strcmp(hoxmlContext->attribute, "points") == 0) {
+ if (raytmxState->object->points != NULL) { /* If there's already an array of points */
+ TraceLog(LOG_WARNING, "RAYTMX: object \"%s\", has multiple 'points' attributes; points listed in any "
+ "latter 'points' attributes will be dropped", raytmxState->object->name);
+ return;
+ }
+ /* The 'points' attribute's value is a string containing all points of the poly(gon|line) in the form */
+ /* "0,0 31.25,-0.75 49.5,-16.5 93.25,-17.25" where the points are [0, 0], [31.25, -0.75], etc. and these */
+ /* points are relative to the object's position (its 'x' and 'y' attributes) */
+ char x[32], y[32], *iterator, *terminator, *comma;
+ iterator = hoxmlContext->value;
+ RaytmxPolyPointNode *pointsRoot = NULL, *pointsTail = NULL;
+ uint32_t pointsLength = 0;
+ Vector2 vertexSum; /* Specific to polygons, the centroid is calculated requiring a sum of all vertices */
+ vertexSum.x = 0;
+ vertexSum.y = 0;
+ while (iterator != NULL) {
+ terminator = strstr(iterator, " "); /* Use this pointer to mark the end of the current point */
+ if (terminator == NULL) { /* There are no space delimiters this far into the value string */
+ /* This is the end of the value string so we'll use a regular null terminator instead */
+ terminator = iterator + strlen(iterator);
+ }
+ comma = strstr(iterator, ","); /* Use this pointer to mark the location of the comma between X and Y */
+ if (comma == NULL) {
+ /* The string is malformed. Cannot continue. */
+ TraceLog(LOG_WARNING, "RAYTMX: The 'points' attribute on line %d has a malformed value; at least "
+ "one point is lost as a result", hoxmlContext->line);
+ break;
+ }
+ /* 'iterator' is pointing to the first digit, 'comma' is pointing to the comma, and 'terminator' is */
+ /* pointing to either the terminating space or a typical null terminator */
+ StringCopyN(x, iterator, comma - iterator); /* Copy from 'iterator' up to but excluding 'comma' */
+ x[comma - iterator] = '\0';
+ iterator = comma + 1; /* Point the iterator right after the ',' where Y begins */
+ StringCopyN(y, iterator, terminator - iterator); /* Copy 'iterator' up to but excluding 'terminator' */
+ y[terminator - iterator] = '\0';
+ /* Create a linked list node to hold the point and append it to the linked list */
+ RaytmxPolyPointNode* node = (RaytmxPolyPointNode*)MemAllocZero(sizeof(RaytmxPolyPointNode));
+ /* Note: These values may be negative. A poly(gon|line) object's position is determined by the first */
+ /* vertex added leading to the first entry to be "0,0" and all other vertices relative to it. */
+ node->point.x = (float)atof(x);
+ node->point.y = (float)atof(y);
+ vertexSum.x += node->point.x;
+ vertexSum.y += node->point.y;
+ if (pointsRoot == NULL) /* If this is the first point to add to the list */
+ pointsRoot = node;
+ else /* If adding to the tail of the list */
+ pointsTail->next = node;
+ pointsTail = node;
+ pointsLength += 1;
+ iterator = terminator[0] != '\0' ? terminator + 1 : NULL;
+ }
+
+ if (pointsRoot != NULL) { /* If a list with at least one node was created from the 'points' attribute */
+ /* The first vertex will be duplicated and appended to the end of the list, for drawing purposes, so */
+ /* the length of the points list is incremented by one */
+ pointsLength += 1;
+ bool isPolygon = strcmp(hoxmlContext->tag, "polygon") == 0;
+ if (isPolygon) { /* If the object is a polygon, not polyline */
+ /* Polygons will be drawn using raylib's DrawTriangleFan() function in which the first point is */
+ /* the centroid. It must also end with the first, non-centroid point. So, for polygons, the list */
+ /* will have two more points. */
+ pointsLength += 1;
+ }
+ /* Allocate the array and assign NULL to every index to be safe */
+ Vector2* points = (Vector2*)MemAllocZero(sizeof(Vector2) * pointsLength);
+ if (isPolygon) { /* If the centroid should be included as a vertex */
+ /* Finish calculating the centroid by averaging the sum of the vertices keeping in mind that */
+ /* 'pointsLength' is equal to N + 2 */
+ points[0].x = vertexSum.x / (pointsLength - 2);
+ points[0].y = vertexSum.y / (pointsLength - 2);
+ }
+ /* Copy the points as Vector2s into the array and free the nodes while we're at it */
+ RaytmxPolyPointNode* iteratorNode = pointsRoot;
+ uint32_t i = isPolygon ? 1 : 0; /* Skip over the first element, the centroid, for polygons only */
+ while (iteratorNode != NULL) {
+ points[i] = iteratorNode->point;
+ RaytmxPolyPointNode* parent = iteratorNode;
+ iteratorNode = iteratorNode->next;
+ i += 1;
+ MemFree(parent);
+ }
+ /* End the list with the first point. Both polygons and polylines use this when drawing. */
+ points[pointsLength - 1].x = points[isPolygon ? 1 : 0].x;
+ points[pointsLength - 1].y = points[isPolygon ? 1 : 0].y;
+ /* TODO: sort the vertices into counter-clockwise order as DrawTriangleFan() requires it */
+ /* Add the points array to the element it applies to */
+ raytmxState->object->points = points;
+ raytmxState->object->pointsLength = pointsLength;
+ raytmxState->object->drawPoints = (Vector2*)MemAllocZero(sizeof(Vector2) * pointsLength);
+ }
+ } /* raytmxState->object != NULL && strcmp(hoxmlContext->attribute, "points") == 0 */
+ } /* strcmp(hoxmlContext->tag, "polygon") == 0 || strcmp(hoxmlContext->tag, "polyline") == 0 */
+ else if (strcmp(hoxmlContext->tag, "text") == 0) {
+ if (raytmxState->object != NULL && raytmxState->object->text != NULL) {
+ if (strcmp(hoxmlContext->attribute, "fontfamily") == 0) {
+ raytmxState->object->text->fontFamily =
+ (char*)MemAllocZero((unsigned int)strlen(hoxmlContext->value) + 1);
+ StringCopy(raytmxState->object->text->fontFamily, hoxmlContext->value);
+ } else if (strcmp(hoxmlContext->attribute, "pixelsize") == 0)
+ raytmxState->object->text->pixelSize = atoi(hoxmlContext->value);
+ else if (strcmp(hoxmlContext->attribute, "wrap") == 0)
+ raytmxState->object->text->wrap = atoi(hoxmlContext->value) != 0 ? true : false;
+ else if (strcmp(hoxmlContext->attribute, "color") == 0)
+ raytmxState->object->text->color = GetColorFromHexString(hoxmlContext->value);
+ else if (strcmp(hoxmlContext->attribute, "bold") == 0)
+ raytmxState->object->text->bold = atoi(hoxmlContext->value) != 0 ? true : false;
+ else if (strcmp(hoxmlContext->attribute, "italic") == 0)
+ raytmxState->object->text->italic = atoi(hoxmlContext->value) != 0 ? true : false;
+ else if (strcmp(hoxmlContext->attribute, "underline") == 0)
+ raytmxState->object->text->underline = atoi(hoxmlContext->value) != 0 ? true : false;
+ else if (strcmp(hoxmlContext->attribute, "strikeout") == 0)
+ raytmxState->object->text->strikeOut = atoi(hoxmlContext->value) != 0 ? true : false;
+ else if (strcmp(hoxmlContext->attribute, "kerning") == 0)
+ raytmxState->object->text->kerning = atoi(hoxmlContext->value) != 0 ? true : false;
+ else if (strcmp(hoxmlContext->attribute, "halign") == 0) {
+ if (strcmp(hoxmlContext->value, "left") == 0)
+ raytmxState->object->text->halign = HORIZONTAL_ALIGNMENT_LEFT;
+ else if (strcmp(hoxmlContext->value, "center") == 0)
+ raytmxState->object->text->halign = HORIZONTAL_ALIGNMENT_CENTER;
+ else if (strcmp(hoxmlContext->value, "right") == 0)
+ raytmxState->object->text->halign = HORIZONTAL_ALIGNMENT_RIGHT;
+ else if (strcmp(hoxmlContext->value, "justify") == 0)
+ raytmxState->object->text->halign = HORIZONTAL_ALIGNMENT_JUSTIFY;
+ } else if (strcmp(hoxmlContext->attribute, "valign") == 0) {
+ if (strcmp(hoxmlContext->value, "top") == 0)
+ raytmxState->object->text->valign = VERTICAL_ALIGNMENT_TOP;
+ else if (strcmp(hoxmlContext->value, "center") == 0)
+ raytmxState->object->text->valign = VERTICAL_ALIGNMENT_CENTER;
+ else if (strcmp(hoxmlContext->value, "bottom") == 0)
+ raytmxState->object->text->valign = VERTICAL_ALIGNMENT_BOTTOM;
+ }
+ } /* raytmxState->object != NULL && raytmxState->object->text != NULL */
+ } /* strcmp(hoxmlContext->tag, "text") == 0 */
+ else if (strcmp(hoxmlContext->tag, "imagelayer") == 0) {
+ if (raytmxState->imageLayer != NULL) {
+ /* Check for attributes specific to <imagelayer> layers */
+ if (strcmp(hoxmlContext->attribute, "repeatx") == 0)
+ raytmxState->imageLayer->repeatX = atoi(hoxmlContext->value) != 0 ? true : false;
+ else if (strcmp(hoxmlContext->attribute, "repeaty") == 0)
+ raytmxState->imageLayer->repeatY = atoi(hoxmlContext->value) != 0 ? true : false;
+ }
+ } /* strcmp(hoxmlContext->tag, "imagelayer") == 0 */
+
+ if (strcmp(hoxmlContext->tag, "layer") == 0 || strcmp(hoxmlContext->tag, "objectgroup") == 0 ||
+ strcmp(hoxmlContext->tag, "imagelayer") == 0 || strcmp(hoxmlContext->tag, "group") == 0) {
+ if (raytmxState->layer != NULL) {
+ /* Check for attributes common to all layer types */
+ if (strcmp(hoxmlContext->attribute, "id") == 0)
+ raytmxState->layer->id = atoi(hoxmlContext->value);
+ else if (strcmp(hoxmlContext->attribute, "name") == 0) {
+ raytmxState->layer->name = (char*)MemAllocZero((unsigned int)strlen(hoxmlContext->value) + 1);
+ StringCopy(raytmxState->layer->name, hoxmlContext->value);
+ } else if (strcmp(hoxmlContext->attribute, "class") == 0) {
+ raytmxState->layer->classString = (char*)MemAllocZero((unsigned int)strlen(hoxmlContext->value) + 1);
+ StringCopy(raytmxState->layer->classString, hoxmlContext->value);
+ } else if (strcmp(hoxmlContext->attribute, "opacity") == 0)
+ raytmxState->layer->opacity = atof(hoxmlContext->value);
+ else if (strcmp(hoxmlContext->attribute, "visible") == 0)
+ raytmxState->layer->visible = atoi(hoxmlContext->value) != 0 ? true : false;
+ else if (strcmp(hoxmlContext->attribute, "tintcolor") == 0) {
+ raytmxState->layer->tintColor = GetColorFromHexString(hoxmlContext->value);
+ raytmxState->layer->hasTintColor = true;
+ } else if (strcmp(hoxmlContext->attribute, "offsetx") == 0)
+ raytmxState->layer->offsetX = atoi(hoxmlContext->value);
+ else if (strcmp(hoxmlContext->attribute, "offsety") == 0)
+ raytmxState->layer->offsetY = atoi(hoxmlContext->value);
+ else if (strcmp(hoxmlContext->attribute, "parallaxx") == 0)
+ raytmxState->layer->parallaxX = atof(hoxmlContext->value);
+ else if (strcmp(hoxmlContext->attribute, "parallaxy") == 0)
+ raytmxState->layer->parallaxY = atof(hoxmlContext->value);
+ }
+ } /* strcmp(hoxmlContext->tag, "layer") == 0 || strcmp(hoxmlContext->tag, "objectgroup") == 0 || */
+ /* strcmp(hoxmlContext->tag, "imagelayer") == 0 || strcmp(hoxmlContext->tag, "group") == 0 */
+}
+
+void HandleElementEnd(RaytmxState* raytmxState, hoxml_context_t* hoxmlContext) {
+ if (raytmxState == NULL || hoxmlContext == NULL)
+ return;
+
+ /* If the element is one of the layer types which share some common attributes that may need default strings */
+ if (strcmp(hoxmlContext->tag, "layer") == 0 || strcmp(hoxmlContext->tag, "objectgroup") == 0 ||
+ strcmp(hoxmlContext->tag, "imagelayer") == 0 || strcmp(hoxmlContext->tag, "group") == 0) {
+ TmxLayer* layer = raytmxState->layer;
+ if (layer == NULL && raytmxState->groupNode != NULL)
+ layer = &raytmxState->groupNode->layer;
+
+ if (layer != NULL) {
+ /* Apply default values for the attribute(s) that aren't covered by a simple memset(x, 0, sizeof(x)) */
+ if (layer->name == NULL) { /* If this layer didn't have a 'name' attribute */
+ /* The default value for 'name' is "" (an empty string) */
+ layer->name = (char*)MemAlloc(1);
+ layer->name[0] = '\0';
+ }
+ if (layer->classString == NULL) { /* If this layer didn't have a 'class' attribute */
+ /* The default value for 'class' is "" (an empty string) */
+ layer->classString = (char*)MemAlloc(1);
+ layer->classString[0] = '\0';
+ }
+ }
+ } /* strcmp(hoxmlContext->tag, "layer") == 0 || strcmp(hoxmlContext->tag, "objectgroup") == 0 || */
+ /* strcmp(hoxmlContext->tag, "imagelayer") == 0 || strcmp(hoxmlContext->tag, "group") == 0 */
+
+ if (strcmp(hoxmlContext->tag, "properties") == 0) {
+ if (raytmxState->propertiesRoot == NULL)
+ return;
+ /* TMX allows nested properties (e.g. <properties><properties><property/></properties></properties>) but */
+ /* that is unsupported. Depth is tracked such that all properties, included nested ones, are appended to one */
+ /* array when the outermost element ends. */
+ raytmxState->propertiesDepth -= 1;
+ if (raytmxState->propertiesDepth > 0) /* If the outermost <properties> has not yet ended */
+ return;
+ /* Allocate the array and assign NULL to every index to be safe */
+ TmxProperty* properties = (TmxProperty*)MemAllocZero(sizeof(TmxProperty) * raytmxState->propertiesLength);
+ /* Copy the TmxProperty pointers into the array and free the nodes while we're at it */
+ RaytmxPropertyNode* iterator = raytmxState->propertiesRoot;
+ for (uint32_t i = 0; iterator != NULL; i++) {
+ properties[i] = iterator->property;
+ RaytmxPropertyNode* parent = iterator;
+ iterator = iterator->next;
+ MemFree(parent);
+ }
+ /* Add the properties array to the element it applies to */
+ /* A <property>, or rather its parent <properties>, can be within 10+ other elements. The order of the checks */
+ /* here is slightly important as the most-nested elements must come first in cases where one may contain */
+ /* another (e.g. an <object> within an <objectgroup> layer). */
+ if (raytmxState->tilesetTile != NULL && raytmxState->tilesetTile->properties == NULL) {
+ raytmxState->tilesetTile->properties = properties;
+ raytmxState->tilesetTile->propertiesLength = raytmxState->propertiesLength;
+ }
+ /* else if (raytmxState->wangColor != NULL && raytmxState->wangColor->properties == NULL) {
+ raytmxState->wangColor->properties = properties;
+ raytmxState->wangColor->propertiesLength = raytmxState->propertiesLength;
+ }
+ else if (raytmxState->wangSet != NULL && raytmxState->wangSet->properties == NULL) {
+ raytmxState->wangSet->properties = properties;
+ raytmxState->wangSet->propertiesLength = raytmxState->propertiesLength;
+ } */ /* TODO: Wang sets. Low priority. */
+ else if (raytmxState->tileset != NULL && raytmxState->tileset->properties == NULL) {
+ raytmxState->tileset->properties = properties;
+ raytmxState->tileset->propertiesLength = raytmxState->propertiesLength;
+ } else if (raytmxState->object != NULL && raytmxState->object->properties == NULL) {
+ raytmxState->object->properties = properties;
+ raytmxState->object->propertiesLength = raytmxState->propertiesLength;
+ } else if (raytmxState->layer != NULL && raytmxState->layer->properties == NULL) {
+ raytmxState->layer->properties = properties;
+ raytmxState->layer->propertiesLength = raytmxState->propertiesLength;
+ } else {
+ raytmxState->mapProperties = properties;
+ raytmxState->mapPropertiesLength = raytmxState->propertiesLength;
+ }
+ /* Clean up the state object */
+ raytmxState->propertiesRoot = NULL;
+ raytmxState->propertiesTail = NULL;
+ raytmxState->propertiesLength = 0;
+ } /* strcmp(hoxmlContext->tag, "properties") == 0 */
+ else if (strcmp(hoxmlContext->tag, "property") == 0) {
+ if (raytmxState->property != NULL) {
+ /* Apply default values for the attribute(s) that aren't covered by a simple memset(x, 0, sizeof(x)) */
+ /* Properties are cast and assigned to type-specific variables at the end of the element due to the order */
+ /* of the property's attributes not being guaranteed, so we wait until we have all the information */
+ switch (raytmxState->property->type) {
+ case PROPERTY_TYPE_STRING:
+ default: /* The default type of a property is 'string' */
+ if (raytmxState->property->stringValue == NULL) {
+ if (hoxmlContext->content != NULL) { /* If Tiled opted to put the value in the element's content */
+ /* From the documentation: "When a string property contains newlines, the current version of */
+ /* Tiled will write out the value as characters contained inside the property element rather */
+ /* than as the value attribute." */
+ raytmxState->property->stringValue =
+ (char*)MemAlloc((unsigned int)strlen(hoxmlContext->content) + 1);
+ StringCopy(raytmxState->property->stringValue, hoxmlContext->content);
+ } else { /* If the string's value was neither provided as an attribute nor content */
+ /* The default value for 'string' is an empty string */
+ raytmxState->property->stringValue = (char*)MemAlloc(1);
+ raytmxState->property->stringValue[0] = '\0';
+ }
+ } break;
+ case PROPERTY_TYPE_INT:
+ case PROPERTY_TYPE_OBJECT:
+ /* The default value for 'int' and 'object' is 0 */
+ raytmxState->property->intValue = atoi(raytmxState->property->stringValue); /* Defaults to 0 */
+ break;
+ case PROPERTY_TYPE_FLOAT:
+ /* The default value for 'float' is 0 */
+ raytmxState->property->floatValue = (float)atof(raytmxState->property->stringValue); /* Defaults to 0 */
+ break;
+ case PROPERTY_TYPE_BOOL:
+ /* The default value for 'bool' is false */
+ if (raytmxState->property->stringValue == NULL ||
+ strcmp(raytmxState->property->stringValue, "true") != 0)
+ raytmxState->property->boolValue = false;
+ else
+ raytmxState->property->boolValue = true;
+ break;
+ case PROPERTY_TYPE_COLOR:
+ /* The default value for 'color' is the color #00000000 */
+ if (raytmxState->property->stringValue == NULL)
+ raytmxState->property->colorValue = BLANK; /* #defined by raylib as { 0, 0, 0, 0 } */
+ else
+ raytmxState->property->colorValue = GetColorFromHexString(raytmxState->property->stringValue);
+ break;
+ case PROPERTY_TYPE_FILE:
+ /* The default value for 'file' is "." */
+ if (raytmxState->property->stringValue == NULL) {
+ raytmxState->property->stringValue = (char*)MemAlloc(2);
+ raytmxState->property->stringValue[0] = '.';
+ raytmxState->property->stringValue[1] = '\0';
+ } break;
+ } /* switch (raytmxState->property->type) */
+
+ /* If the type was neither 'string' nor 'file' and 'stringValue' is set */
+ if (raytmxState->property->type != PROPERTY_TYPE_STRING &&
+ raytmxState->property->type != PROPERTY_TYPE_FILE && raytmxState->property->stringValue != NULL) {
+ /* Properties of types other than 'string' and 'file' are placed in 'stringValue' temporarily. Now */
+ /* that they have been cast and assigned appropriately, 'stringValue' can be freed. */
+ MemFree(raytmxState->property->stringValue);
+ raytmxState->property->stringValue = NULL;
+ }
+ }
+ raytmxState->property = NULL;
+ } /* strcmp(hoxmlContext->tag, "property") == 0 */
+ else if (strcmp(hoxmlContext->tag, "tileset") == 0) {
+ if (raytmxState->tileset != NULL) {
+ /* Apply default values for the attribute(s) that aren't covered by a simple memset(x, 0, sizeof(x)) */
+ if (raytmxState->tileset->name == NULL) { /* If this <tileset> didn't have a 'name' attribute */
+ /* The default value for 'name' is "" (an empty string) */
+ raytmxState->tileset->name = (char*)MemAlloc(1);
+ raytmxState->tileset->name[0] = '\0';
+ }
+ if (raytmxState->tileset->classString == NULL) { /* If this <tileset> didn't have a 'class' attribute */
+ /* The default value for 'class' is "" (an empty string) */
+ raytmxState->tileset->classString = (char*)MemAlloc(1);
+ raytmxState->tileset->classString[0] = '\0';
+ }
+ if (raytmxState->tileset->objectAlignment == OBJECT_ALIGNMENT_UNSPECIFIED) {
+ /* There are default object alignments for orthogonal and isometric modes */
+ if (raytmxState->mapOrientation == ORIENTATION_ORTHOGONAL)
+ raytmxState->tileset->objectAlignment = OBJECT_ALIGNMENT_BOTTOM_LEFT;
+ else if (raytmxState->mapOrientation == ORIENTATION_ISOMETRIC)
+ raytmxState->tileset->objectAlignment = OBJECT_ALIGNMENT_BOTTOM;
+ }
+
+ if (raytmxState->tilesetTilesRoot != NULL) {
+ /* Allocate the array and zeroize every index as initialization */
+ TmxTilesetTile* tiles = (TmxTilesetTile*)MemAllocZero(sizeof(TmxTilesetTile) *
+ raytmxState->tilesetTilesLength);
+ /* Copy the TmxTilesetTile pointers into the array and free the nodes while we're at it */
+ RaytmxTilesetTileNode* iterator = raytmxState->tilesetTilesRoot;
+ for (uint32_t i = 0; i < raytmxState->tilesetTilesLength && iterator != NULL; i++) {
+ tiles[i] = iterator->tile;
+ RaytmxTilesetTileNode* parent = iterator;
+ iterator = iterator->next;
+ MemFree(parent);
+ }
+ /* Add the tiles array to the tileset */
+ raytmxState->tileset->tiles = tiles;
+ raytmxState->tileset->tilesLength = raytmxState->tilesetTilesLength;
+ /* Clean up the state object */
+ raytmxState->tilesetTilesRoot = NULL;
+ raytmxState->tilesetTilesTail = NULL;
+ raytmxState->tilesetTilesLength = 0;
+ }
+ }
+ raytmxState->tileset = NULL;
+ } /* strcmp(hoxmlContext->tag, "tileset") == 0 */
+ else if (strcmp(hoxmlContext->tag, "image") == 0)
+ raytmxState->image = NULL;
+ else if (strcmp(hoxmlContext->tag, "animation") == 0) {
+ if (raytmxState->tilesetTile != NULL && raytmxState->tilesetTile->hasAnimation) {
+ if (raytmxState->animationFramesRoot == NULL)
+ return;
+ /* Allocate the array and zeroize every index as initialization */
+ TmxAnimationFrame* frames = (TmxAnimationFrame*)MemAllocZero(sizeof(TmxAnimationFrame) *
+ raytmxState->animationFramesLength);
+ /* Copy the TmxAnimationFrame pointers into the array and free the nodes while we're at it */
+ RaytmxAnimationFrameNode* iterator = raytmxState->animationFramesRoot;
+ for (uint32_t i = 0; iterator != NULL; i++) {
+ frames[i] = iterator->frame;
+ RaytmxAnimationFrameNode* parent = iterator;
+ iterator = iterator->next;
+ MemFree(parent);
+ }
+ /* Add the frames array to the tile's animation */
+ raytmxState->tilesetTile->animation.frames = frames;
+ raytmxState->tilesetTile->animation.framesLength = raytmxState->animationFramesLength;
+ /* Clean up the state object */
+ raytmxState->animationFramesRoot = NULL;
+ raytmxState->animationFramesTail = NULL;
+ raytmxState->animationFramesLength = 0;
+ }
+ } /* strcmp(hoxmlContext->tag, "animation") == 0 */
+ else if (strcmp(hoxmlContext->tag, "frame") == 0)
+ raytmxState->animationFrame = NULL;
+ else if (strcmp(hoxmlContext->tag, "layer") == 0) {
+ if (raytmxState->tileLayer != NULL) {
+ /* If there were 1+ <tile>s within this <layer> but this <layer> already has tiles (from a <data>?) */
+ if (raytmxState->layerTilesRoot != NULL && raytmxState->tileLayer->tiles != NULL) {
+ TraceLog(LOG_WARNING, "RAYTMX: layer \"%s\" has more than one source of tile data - the latter tiles "
+ "for this layer will be dropped", raytmxState->layer->name);
+ /* Free the nodes and tiles therein */
+ RaytmxTileLayerTileNode* iterator = raytmxState->layerTilesRoot;
+ while (iterator != NULL) {
+ RaytmxTileLayerTileNode* parent = iterator;
+ iterator = iterator->next;
+ MemFree(parent);
+ }
+ } else {
+ /* Allocate the array and zeroize every index as initialization */
+ uint32_t* tiles = (uint32_t*)MemAllocZero(sizeof(uint32_t) * raytmxState->layerTilesLength);
+ /* Copy the GID into the array and free the nodes while we're at it */
+ RaytmxTileLayerTileNode* iterator = raytmxState->layerTilesRoot;
+ for (uint32_t i = 0; iterator != NULL; i++) {
+ tiles[i] = iterator->gid;
+ RaytmxTileLayerTileNode* parent = iterator;
+ iterator = iterator->next;
+ MemFree(parent);
+ }
+ /* Add the tiles array to the tile layer */
+ raytmxState->tileLayer->tiles = tiles;
+ raytmxState->tileLayer->tilesLength = raytmxState->layerTilesLength;
+ }
+ /* Clean up the state object */
+ raytmxState->layerTilesRoot = NULL;
+ raytmxState->layerTilesTail = NULL;
+ raytmxState->layerTilesLength = 0;
+ }
+ raytmxState->tileLayer = NULL;
+ raytmxState->layer = NULL;
+ } /* strcmp(hoxmlContext->tag, "layer") == 0 */
+ else if (strcmp(hoxmlContext->tag, "tile") == 0) {
+ if (raytmxState->tilesetTile != NULL) {
+ /* Apply default values for the attribute(s) that aren't covered by a simple memset(x, 0, sizeof(x)) */
+ if (raytmxState->tilesetTile->classString == NULL) { /* If this <tile> didn't have a 'class' attribute */
+ /* The default value for 'class' is "" (an empty string) */
+ raytmxState->tilesetTile->classString = (char*)MemAlloc(1);
+ raytmxState->tilesetTile->classString[0] = '\0';
+ }
+ if (raytmxState->tilesetTile->hasImage) {
+ /* The 'width' and 'height' attributes default to the tile's image's width and height, respectively */
+ if (raytmxState->tilesetTile->width == 0)
+ raytmxState->tilesetTile->width = raytmxState->tilesetTile->image.width;
+ if (raytmxState->tilesetTile->height == 0)
+ raytmxState->tilesetTile->height = raytmxState->tilesetTile->image.height;
+ }
+ raytmxState->tilesetTile = NULL;
+ }
+ } /* strcmp(hoxmlContext->tag, "tile") == 0 */
+ else if (strcmp(hoxmlContext->tag, "data") == 0) {
+ if (raytmxState->image != NULL) {
+ /* TODO (?): The TMX map format documentation says an <image> can contain a <data> element but doesn't */
+ /* provide any more information than that. Tiled doesn't seem to have a feature for this either. */
+ } else if (raytmxState->tileLayer != NULL && raytmxState->tileLayer->tiles != NULL) {
+ TraceLog(LOG_WARNING, "RAYTMX: layer \"%s\" has more than one source of tile data - the latter tiles for "
+ "this layer will be dropped", raytmxState->layer->name);
+ } else if (raytmxState->tileLayer != NULL && raytmxState->tileLayer->encoding != NULL) {
+ RaytmxTileLayerTileNode* tiles = NULL;
+ if (strcmp(raytmxState->tileLayer->encoding, "base64") == 0) {
+ /* The layer's data is a series of unsigned, 32-bit integers encoded as a Base64 string. But, XML */
+ /* considers everything between <data> and </data> to be content meaning there is probably some */
+ /* whitespace on both ends of the content we need to ignore. So, find the actual start and stop: */
+ char *encodedStart = hoxmlContext->content, *encodedEnd;
+ while (isspace(*encodedStart))
+ encodedStart++;
+ encodedEnd = encodedStart + strlen(encodedStart) - 1;
+ while (encodedEnd > encodedStart && isspace(*encodedEnd))
+ encodedEnd--;
+
+ /* With the string of encoded Base64 data trimmed, decode it */
+ int decodedLength;
+ unsigned char* decoded = DecodeDataBase64((const unsigned char*)encodedStart, &decodedLength);
+ if (decoded != NULL) {
+ if (raytmxState->tileLayer->compression == NULL) { /* If the Base64-encoded data is uncompressed */
+ /* Iterate through N bytes ('decodedLength') with every four bytes being a single GID */
+ /* resulting in N / 4 tiles */
+ uint32_t* iterator = (uint32_t*)decoded;
+ for (int i = 0; i < decodedLength / 4; i++) {
+ AddTileLayerTile(raytmxState, *iterator);
+ iterator += 1; /* Point to the next unsigned, 32-bit integer in the decoded data */
+ }
+ } else { /* If the Base-64encoded data is also compressed */
+ if (strcmp(raytmxState->tileLayer->compression, "gzip") == 0 ||
+ strcmp(raytmxState->tileLayer->compression, "zlib") == 0) {
+ unsigned char* postHeaderDecoded = NULL;
+ if (strcmp(raytmxState->tileLayer->compression, "gzip") == 0) {
+ /* The first two bytes of a GZIP header are expected to be a magic number, 0x1F8B, */
+ /* identifying the format and the third is expected to indicate the compression */
+ /* method where 0x08 is DEFLATE. */
+ /* If these values are found, decompression can continue */
+ if (decoded[0] == 0x1F && decoded[1] == 0x8B && decoded[2] == 0x08) {
+ /* Skip past the GZIP header. The header is typically ten bytes. The bytes not */
+ /* checked are unimportant things like a timestamp and OS ID. Additional optional */
+ /* headers are possible but not used by Tiled so they are assumed to be missing. */
+ postHeaderDecoded = decoded + 10;
+ } else { /* If the GZIP header doesn't match a decompressable one */
+ TraceLog(LOG_ERROR, "RAYTMX: Layer \"%s\" uses GZIP compression but the stream's "
+ "header doesn't indicate DEFLATE compression", raytmxState->layer->name);
+ }
+ } else /* if (strcmp(raytmxState->tileLayer->compression, "zlib") == 0) */ {
+ /* The first byte of a ZLIB header is expected to be 0x78 where the 8 indicates the */
+ /* DEFLATE compression method and the 7 is "compression info" that indicates a 32K */
+ /* LZ77 window size and, in practice, cannot be anything else. */
+ /* If these values are found, decompression can continue */
+ if (decoded[0] == 0x78) {
+ /* Skip past the ZLIB header. The header is two bytes. */
+ postHeaderDecoded = decoded + 2;
+ } else { /* If the ZLIB header doesn't match a decompressable one */
+ TraceLog(LOG_ERROR, "RAYTMX: Layer \"%s\" uses ZLIB compression but the stream's "
+ "header doesn't indicate DEFLATE compression", raytmxState->layer->name);
+ }
+ }
+
+ if (postHeaderDecoded != NULL) {
+ /* "zlib" and "gzip" both use the DEFLATE algorithm and raylib provides a */
+ /* decompression function when it's built with SUPPORT_COMPRESSION_API (default) */
+ int decompressedLength;
+ unsigned char* decompressed = DecompressData(postHeaderDecoded, decodedLength,
+ &decompressedLength);
+ if (decompressed != NULL && decompressedLength > 0) {
+ uint32_t* iterator = (uint32_t*)decompressed;
+ for (int i = 0; i < decompressedLength / 4; i++) {
+ AddTileLayerTile(raytmxState, *iterator);
+ iterator += 1; /* Point to the next unsigned integer in the decompressed data */
+ }
+ MemFree(decompressed); /* Free the memory allocated by DecompressData() */
+ } else { /* raylib wasn't built with compression or allocation failed */
+ TraceLog(LOG_ERROR, "RAYTMX: Layer \"%s\" compressed with \"%s\" cannot be parsed "
+ "because DEFLATE decompression failed - either raylib was not built with "
+ "SUPPORT_COMPRESSION_API or memory allocation failed", raytmxState->layer->name,
+ raytmxState->tileLayer->compression);
+ }
+ }
+ } else {
+ TraceLog(LOG_ERROR, "RAYTMX: Layer \"%s\" cannot be parsed because the compression method "
+ "\"%s\" is unsupported", raytmxState->layer->name, raytmxState->tileLayer->compression);
+ }
+ }
+ MemFree(decoded); /* Free the memory allocated by DecodeDataBase64() */
+ } else {
+ TraceLog(LOG_ERROR, "RAYTMX: Unable to decode Base64 data for layer \"%s\"",
+ raytmxState->layer->name);
+ }
+ } /* strcmp(raytmxState->tileLayer->encoding, "base64") == 0 */
+ else if (strcmp(raytmxState->tileLayer->encoding, "csv") == 0) {
+ /* The Comma-Separated Value (CSV) list herein is a series of Global IDs (GIDs) of tiles in the form */
+ /* "31,32,33" where 31, 32, and 33 are GIDs */
+ char valueAsString[16]; /* Needs to fit all digits of a single value - should be more than enough */
+ char* iterator = hoxmlContext->content;
+ while (iterator != NULL && *iterator != '\0') { /* While not pointing to the end of the string */
+ memset(valueAsString, '\0', 16); /* Reset the value-as-a-string buffer with all zeroes */
+ /* Copy each character into the buffer until either a comma or the terminator is reached */
+ for (int i = 0; *iterator != ',' && *iterator != '\0'; i++) {
+ memcpy(valueAsString + i, iterator, 1);
+ iterator++;
+ }
+ if (*iterator == ',') /* If iteration was paused by a comma */
+ iterator++; /* Advance one character so the comma isn't parsed with the value next iteration */
+ AddTileLayerTile(raytmxState, atoi(valueAsString)); /* Read the value as an integer GID */
+ }
+ } /* strcmp(raytmxState->tileLayer->encoding, "csv") == 0 */
+
+ if (tiles != NULL) { /* If there was no error in parsing the data and there's a linked list of tiles */
+ /* Allocate the array and assign 0 to every index to be safe */
+ uint32_t* tiles = (uint32_t*)MemAlloc(sizeof(uint32_t) * raytmxState->layerTilesLength);
+ memset(tiles, 0, sizeof(uint32_t) * raytmxState->layerTilesLength);
+ /* Copy the GIDs into the array and free the nodes while we're at it */
+ RaytmxTileLayerTileNode* layerTilesIterator = raytmxState->layerTilesRoot;
+ for (uint32_t i = 0; i < raytmxState->layerTilesLength && layerTilesIterator != NULL; i++) {
+ tiles[i] = layerTilesIterator->gid;
+ RaytmxTileLayerTileNode* layerTilesTemp = layerTilesIterator;
+ layerTilesIterator = layerTilesIterator->next;
+ MemFree(layerTilesTemp);
+ }
+ /* Add the tiles array to the element it applies to */
+ raytmxState->tileLayer->tiles = tiles;
+ raytmxState->tileLayer->tilesLength = raytmxState->layerTilesLength;
+ /* Clean up the state object */
+ raytmxState->layerTilesRoot = NULL;
+ raytmxState->layerTilesTail = NULL;
+ raytmxState->layerTilesLength = 0;
+ }
+ } /* raytmxState->tileLayer != NULL && raytmxState->tileLayer->encoding != NULL */
+ } /* strcmp(hoxmlContext->tag, "data") == 0 */
+ else if (strcmp(hoxmlContext->tag, "objectgroup") == 0) {
+ if (raytmxState->objectGroup != NULL) {
+ if (raytmxState->objectsRoot == NULL)
+ return;
+ /* Allocate the arrays and zeroize every index as initialization */
+ TmxObject* objects = (TmxObject*)MemAllocZero(sizeof(TmxObject) * raytmxState->objectsLength);
+ uint32_t* ySortedObjects = (uint32_t*)MemAllocZero(sizeof(uint32_t) * raytmxState->objectsLength);
+ /* Create a contiguous array of TmxObjects, create a sorted linked list of indexes within that array of */
+ /* TmxObjects (sorted by ascending y-coordinate), and free the object linked list */
+ RaytmxObjectNode *objectsIterator = raytmxState->objectsRoot, *objectsTemp;
+ RaytmxObjectSortingNode *sortingRoot = NULL, *sortingIterator, *sortingTemp, *newSortingNode;
+ for (uint32_t i = 0; objectsIterator != NULL; i++) {
+ objects[i] = objectsIterator->object;
+ /* Add a new node into the sorted list */
+ newSortingNode = (RaytmxObjectSortingNode*)MemAllocZero(sizeof(RaytmxObjectSortingNode));
+ newSortingNode->y = objects[i].y;
+ newSortingNode->index = i;
+ if (sortingRoot == NULL) /* If this is the first node */
+ sortingRoot = newSortingNode;
+ else if (sortingRoot->y >= newSortingNode->y) { /* If replacing the root node */
+ newSortingNode->next = sortingRoot;
+ sortingRoot = newSortingNode;
+ } else { /* If inserting at some location after the root */
+ sortingIterator = sortingRoot;
+ while (sortingIterator->next != NULL && newSortingNode->y > sortingIterator->next->y)
+ sortingIterator = sortingIterator->next;
+ newSortingNode->next = sortingIterator->next;
+ sortingIterator->next = newSortingNode;
+ }
+ /* Free the object node */
+ objectsTemp = objectsIterator;
+ objectsIterator = objectsIterator->next;
+ MemFree(objectsTemp);
+ }
+ /* Create a contiguous array from the sorted linked list such that index 0 of this array points to the */
+ /* TmxObject (via its index in 'objects') with the lowest (visually, highest) y-coordinate */
+ sortingIterator = sortingRoot;
+ for (uint32_t i = 0; sortingIterator != NULL; i++) {
+ ySortedObjects[i] = sortingIterator->index;
+ sortingTemp = sortingIterator;
+ sortingIterator = sortingIterator->next;
+ MemFree(sortingTemp);
+ }
+ /* Add the objects and ySortedObjects array to the object layer */
+ raytmxState->objectGroup->objects = objects;
+ raytmxState->objectGroup->objectsLength = raytmxState->objectsLength;
+ raytmxState->objectGroup->ySortedObjects = ySortedObjects;
+ /* Clean up the state object */
+ raytmxState->objectsRoot = NULL;
+ raytmxState->objectsTail = NULL;
+ raytmxState->objectsLength = 0;
+ }
+ raytmxState->objectGroup = NULL;
+ raytmxState->layer = NULL;
+ } else if (strcmp(hoxmlContext->tag, "object") == 0) {
+ if (raytmxState->object != NULL) {
+ /* Apply default values for the attribute(s) that aren't covered by a simple memset(x, 0, sizeof(x)) */
+ if (raytmxState->object->name == NULL) { /* If this <object> didn't have a 'name' attribute */
+ /* The default value for 'name' is "" (an empty string) */
+ raytmxState->object->name = (char*)MemAlloc(1);
+ raytmxState->object->name[0] = '\0';
+ }
+ if (raytmxState->object->typeString == NULL) { /* If this <object> didn't have a 'type' attribute */
+ /* The default value for 'type' is "" (an empty string) */
+ raytmxState->object->typeString = (char*)MemAlloc(1);
+ raytmxState->object->typeString[0] = '\0';
+ }
+
+ if (raytmxState->object->templateString != NULL) {
+ /* During editing, objects can be created from templates with pre-defined values and/or <properties>, */
+ /* a concept similar to copying and pasting or instancing. These templates are saved in external TX */
+ /* files that the 'template' attribute points to. Try to load it. */
+ RaytmxCachedTemplateNode* cachedTemplate = LoadCachedTemplate(raytmxState,
+ raytmxState->object->templateString);
+ if (cachedTemplate != NULL) {
+ RaytmxObjectTemplate objectTemplate = cachedTemplate->objectTemplate;
+ /* The template's <object> will contain some subset of values and/or <properties> that apply to */
+ /* the instanced <object> referencing it. For example, if the template's <object> has a 'name' */
+ /* attribute with the value "cactus" then the instanced <object> will also have a 'name' */
+ /* attribute. "cactus" is a default value that will be used if the instanced <object> does not */
+ /* define one of its own. The template's <object> needs to be checked for non-default values and */
+ /* <properties> and they need to be applied to the instanced <object> where none exist. */
+ if (objectTemplate.object.name != NULL && raytmxState->object->name == NULL) {
+ raytmxState->object->name =
+ (char*)MemAllocZero((unsigned int)strlen(objectTemplate.object.name) + 1);
+ StringCopy(raytmxState->object->name, objectTemplate.object.name);
+ }
+ if (objectTemplate.object.typeString != NULL && raytmxState->object->typeString != NULL) {
+ raytmxState->object->typeString =
+ (char*)MemAllocZero((unsigned int)strlen(objectTemplate.object.typeString) + 1);
+ StringCopy(raytmxState->object->typeString, objectTemplate.object.typeString);
+ }
+ if (objectTemplate.object.x != 0.0 && raytmxState->object->x == 0.0)
+ raytmxState->object->x = objectTemplate.object.x;
+ if (objectTemplate.object.y != 0.0 && raytmxState->object->y == 0.0)
+ raytmxState->object->y = objectTemplate.object.y;
+ if (objectTemplate.object.width != 0.0 && raytmxState->object->width == 0.0)
+ raytmxState->object->width = objectTemplate.object.width;
+ if (objectTemplate.object.height != 0.0 && raytmxState->object->height == 0.0)
+ raytmxState->object->height = objectTemplate.object.height;
+ if (objectTemplate.object.rotation != 0.0 && raytmxState->object->rotation == 0.0)
+ raytmxState->object->rotation = objectTemplate.object.rotation;
+ if (objectTemplate.object.gid != 0 && raytmxState->object->gid == 0)
+ raytmxState->object->gid = objectTemplate.object.gid;
+ if (!objectTemplate.object.visible && raytmxState->object->visible)
+ raytmxState->object->visible = false;
+ /* TMX documentation doesn't specify some particulars of templates, namely if it is possible for */
+ /* a template to be type-specific. In other words, can a template be a polygon? If so, are points */
+ /* merged or replaced? Based on Tiled's behavior, templates do not have type-specific values or, */
+ /* at least, they are ignored if they exist. However, <properties> are definitely merged. */
+ /* If the template's <object> has <properties> */
+ if (objectTemplate.object.properties != NULL) {
+ /* There are two cases here: the instanced <object> already has properties, or it doesn't */
+ if (raytmxState->object->properties == NULL) { /* If the instance doesn't have <properties> */
+ /* This is the easy case. Just copy the pointer to the existing array. */
+ raytmxState->object->properties = objectTemplate.object.properties;
+ raytmxState->object->propertiesLength = objectTemplate.object.propertiesLength;
+ } else {
+ /* The two <properties> need to be merged keeping in mind that they may, or probably, */
+ /* have overlapping entries. (When both have properties of the same name, the instanced */
+ /* <object>'s takes priority.) Create a linked list from which to create this new, merged */
+ /* properties array. */
+ RaytmxPropertyNode *propertiesRoot = NULL, *propertiesTail = NULL, *node;
+ uint32_t propertiesLength = 0;
+ /* Add the properties from the instanced <object> */
+ for (uint32_t i = 0; i < raytmxState->object->propertiesLength; i++) {
+ node = (RaytmxPropertyNode*)MemAllocZero(sizeof(RaytmxPropertyNode));
+ node->property = raytmxState->object->properties[i];
+ if (propertiesRoot == NULL)
+ propertiesRoot = node;
+ else
+ propertiesTail->next = node;
+ propertiesTail = node;
+ propertiesLength += 1;
+ }
+ /* Add the properties from the template's <object> if they do not already exist */
+ for (uint32_t i = 0; i < objectTemplate.object.propertiesLength; i++) {
+ RaytmxPropertyNode* propertiesIterator = propertiesRoot;
+ bool isNew = true;
+ while (propertiesIterator != NULL) {
+ if (strcmp(objectTemplate.object.properties[i].name,
+ propertiesIterator->property.name) == 0) {
+ isNew = false;
+ break;
+ }
+ propertiesIterator = propertiesIterator->next;
+ }
+ if (isNew) {
+ node = (RaytmxPropertyNode*)MemAllocZero(sizeof(RaytmxPropertyNode));
+ node->property = objectTemplate.object.properties[i];
+ if (propertiesRoot == NULL)
+ propertiesRoot = node;
+ else
+ propertiesTail->next = node;
+ propertiesTail = node;
+ propertiesLength += 1;
+ }
+ }
+ /* Free the separate array that was previously allocated */
+ MemFree(raytmxState->object->properties);
+ /* Allocate a new array to be populated with the merged properties */
+ raytmxState->object->properties =
+ (TmxProperty*)MemAllocZero(sizeof(TmxProperty) * propertiesLength);
+ raytmxState->object->propertiesLength = propertiesLength;
+ /* Copy the TmxProperty entires into the array and free the nodes while we're at it */
+ RaytmxPropertyNode* propertiesIterator = propertiesRoot;
+ for (uint32_t i = 0; propertiesIterator != NULL; i++) {
+ raytmxState->object->properties[i] = propertiesIterator->property;
+ RaytmxPropertyNode* propertiesTemp = propertiesIterator;
+ propertiesIterator = propertiesIterator->next;
+ MemFree(propertiesTemp);
+ }
+ }
+ }
+ } else
+ TraceLog(LOG_WARNING, "RAYTMX: Unable to apply template to object ID %u", raytmxState->object->id);
+ }
+
+ /* Calculate the Axis-Aligned Bounding Box (AABB) of the object. This can vary by object type. Due to the */
+ /* possibility of 'width' and 'height' being derived from a template, these must be calculated after a */
+ /* template is applied to the object. */
+ switch (raytmxState->object->type) {
+ case OBJECT_TYPE_RECTANGLE:
+ case OBJECT_TYPE_ELLIPSE:
+ case OBJECT_TYPE_TEXT:
+ raytmxState->object->aabb.x = (float)raytmxState->object->x;
+ raytmxState->object->aabb.y = (float)raytmxState->object->y;
+ raytmxState->object->aabb.width = (float)raytmxState->object->width;
+ raytmxState->object->aabb.height = (float)raytmxState->object->height;
+ break;
+ case OBJECT_TYPE_POINT:
+ {
+ raytmxState->object->aabb.x = (float)raytmxState->object->x;
+ raytmxState->object->aabb.y = (float)raytmxState->object->y;
+ raytmxState->object->aabb.width = 0.0f;
+ raytmxState->object->aabb.height = 0.0f;
+ }
+ break;
+ case OBJECT_TYPE_POLYGON:
+ case OBJECT_TYPE_POLYLINE:
+ {
+ float minX = INFINITY, maxX = -INFINITY, minY = INFINITY, maxY = -INFINITY;
+ for (uint32_t i = 1; i < raytmxState->object->pointsLength; i++) {
+ Vector2 point = raytmxState->object->points[i];
+ if (point.x < minX)
+ minX = point.x;
+ if (point.x > maxX)
+ maxX = point.x;
+ if (point.y < minY)
+ minY = point.y;
+ if (point.y > maxY)
+ maxY = point.y;
+ }
+ /* Note: Poly(gon|line) objects' vertices are stored with relative positions whereas AABBs use */
+ /* absolute values. The object's X and Y values must be added. */
+ raytmxState->object->aabb.x = minX + (float)raytmxState->object->x;
+ raytmxState->object->aabb.y = minY + (float)raytmxState->object->y;
+ raytmxState->object->aabb.width = maxX - minX;
+ raytmxState->object->aabb.height = maxY - minY;
+ }
+ break;
+ case OBJECT_TYPE_TILE:
+ /* The tile object type can have varying sizes, depending on the tile. While most will have the width */
+ /* and height defined in the top-level <map>, a "collection of images" tileset will have tiles with */
+ /* arbitrary dimensions. For that reason, an AABB cannot be calculated. */
+ break;
+ }
+ }
+ raytmxState->object = NULL;
+ } /* strcmp(hoxmlContext->tag, "object") == 0 */
+ else if (strcmp(hoxmlContext->tag, "text") == 0) {
+ /* Apply default values for the attribute(s) that aren't covered by a simple memset(x, 0, sizeof(x)) */
+ if (raytmxState->object != NULL && raytmxState->object->text != NULL) {
+ TmxObject* object = raytmxState->object;
+ TmxText* objectText = object->text;
+ if (hoxmlContext->content != NULL) { /* If the element had content e.g. <text>Content here</text> */
+ objectText->content = (char*)MemAllocZero((unsigned int)strlen(hoxmlContext->content) + 1);
+ StringCopy(objectText->content, hoxmlContext->content);
+ }
+
+ if (objectText->fontFamily == NULL) { /* If this <text> didn't have a 'fontfamily' attribute */
+ /* The default value for 'fontfamily' is "sans-serif" */
+ objectText->fontFamily = (char*)MemAllocZero((unsigned int)strlen("sans-serif") + 1);
+ StringCopy(objectText->fontFamily, "sans-serif");
+ }
+
+ if (objectText->content != NULL) { /* If there's text to be drawn */
+ RaytmxTextLineNode *linesRoot = NULL, *linesTail = NULL;
+ uint32_t linesLength = 0;
+
+ /* There's some aligning and allocating to be done in order to draw the text. This isn't something */
+ /* that should be done per-draw and, given the text is static, can be done ahead of time. */
+ Font font = GetFontDefault();
+ float spacing = 1.0f * (objectText->kerning ? 1.0f : 0.0f);
+
+ unsigned int bufferLength = (unsigned int)strlen(objectText->content) + 1;
+ /* This buffer will hold hold subsets of the content while iterating through it. The string in this */
+ /* buffer may exceed the bounds. */
+ char* testingBuffer = (char*)MemAllocZero(bufferLength);
+ /* This one will hold the last known good string whose graphical text would fit within the bounds */
+ char* validBuffer = (char*)MemAllocZero(bufferLength);
+ /* This one will hold space-delimited subsets of the above */
+ char* delimtedBuffer = (char*)MemAllocZero(bufferLength);
+ bool isDelimited = false;
+
+ char *start = objectText->content, *end = start, *validEnd = start, *delimitedEnd = start;
+ /* While the 'end' iterator hasn't reached the end of the content AND further lines will fit within */
+ /* the Y bounds of the object */
+ while (*end != '\0' &&
+ object->y + (objectText->pixelSize * (linesLength + 1)) <= object->y + object->height) {
+ end++;
+ int length = (int)(end - start);
+ if (length <= 0) /* Quick error check for a case that is hopefully impossible in practice */
+ continue;
+
+ StringCopyN(testingBuffer, start, length);
+ /* Remove any trailing whitespace by iterating backwards and adding a terminator in place of any */
+ /* whitespace character */
+ for (char* i = testingBuffer + length - 1; i > testingBuffer && isspace(*i); i--)
+ *i = '\0';
+
+ /* Measure the dimensions of the now-widest text */
+ Vector2 textSize = MeasureTextEx(font, testingBuffer, (float)objectText->pixelSize, spacing);
+ /* If this text still fits within the width of the object (the bounds) */
+ if (textSize.x <= object->width) {
+ /* This string is still valid. Remember the string and where it ends. Iteration may return to */
+ /* last known good position. */
+ validEnd = end;
+ StringCopy(validBuffer, testingBuffer);
+ /* Note: Because the testing buffer is stripped of trailing whitespace, so too is this one */
+
+ /* If the end of the current string is whitespace but the last character wasn't */
+ if (isspace(*end) && !isspace(*(end - 1))) {
+ /* Taking "Hello, from TMX!" as an example, this would be triggered by the first space */
+ /* with "Hello," being the string to remember. In the event that the would-be text is too */
+ /* wide, meaning "Hello, from" is too long to fit width-wise, then this delimted "Hello," */
+ /* will be the line and iteration will return to "from." */
+ isDelimited = true;
+ delimitedEnd = end;
+ StringCopy(delimtedBuffer, validBuffer);
+ }
+ }
+
+ /* If it's time to create a visual line of text from the string either because: 1) the string has */
+ /* become too wide to fit in the bounds of the object, or 2) the end of the content was reached */
+ if (textSize.x > object->width || *end == '\0') {
+ char* sourceBuffer;
+ if (*end == '\0' || !isDelimited) {
+ /* The valid buffer is used in cases where the would-be text is simply the remainder of */
+ /* the content or the line does not have a clear separator (whitespace), like an extra */
+ /* long word that continues to the next line */
+ sourceBuffer = validBuffer;
+ start = validEnd;
+ } else {
+ /* The delimited buffer is used in cases where one or more words were identified using */
+ /* space between them as a delimiter */
+ sourceBuffer = delimtedBuffer;
+ start = delimitedEnd;
+ /* Skip over any whitespace at the delimiter */
+ while (isspace(*start) && *start != '\0')
+ start++;
+ }
+
+ /* The 'start' pointer is pointing to the first character of the next line. Point 'end' */
+ /* to the same place and continue from there so nothing is skipped. Note: 'end' will be */
+ /* incremented at the start of the next loop. */
+ end = start;
+
+ TmxTextLine line;
+ line.content = (char*)MemAllocZero((unsigned int)strlen(sourceBuffer) + 1);
+ StringCopy(line.content, sourceBuffer);
+ line.font = font;
+ line.spacing = spacing;
+ /* Note: The number of lines is not yet known but needs to be for Y positioning */
+
+ RaytmxTextLineNode* node = (RaytmxTextLineNode*)MemAllocZero(sizeof(RaytmxTextLineNode));
+ node->line = line;
+ if (linesRoot == NULL)
+ linesRoot = node;
+ else
+ linesTail->next = node;
+ linesTail = node;
+ linesLength += 1;
+
+ /* Reset variables */
+ memset(testingBuffer, '\0', bufferLength);
+ memset(validBuffer, '\0', bufferLength);
+ memset(delimtedBuffer, '\0', bufferLength);
+ isDelimited = false;
+
+ if (!objectText->wrap) { /* If word wrapping is disabled */
+ /* It's unclear why this would be done but, having hit the end of what can be displayed */
+ /* on a single line, no more text can be appended */
+ break;
+ }
+ } /* textSize.x > object->width || *end == '\0' */
+ } /* *end != '\0' && */
+ /* object->y + (objectText->pixelSize * (linesLength + 1)) <= object->y + object->height */
+
+ MemFree(testingBuffer);
+ MemFree(validBuffer);
+ MemFree(delimtedBuffer);
+
+ if (linesRoot != NULL) {
+ /* Allocate the array and zero out every value as initialization */
+ TmxTextLine* lines = (TmxTextLine*)MemAllocZero(sizeof(TmxTextLine) * linesLength);
+ /* Copy the TmxTextLines into the array and free the nodes while we're at it */
+ RaytmxTextLineNode* iterator = linesRoot;
+ for (uint32_t i = 0; i < linesLength && iterator != NULL; i++) {
+ lines[i] = iterator->line;
+ Vector2 textSize = MeasureTextEx(font, lines[i].content, (float)objectText->pixelSize,
+ lines[i].spacing);
+
+ /* Horizontal alignment */
+ if (objectText->halign == HORIZONTAL_ALIGNMENT_RIGHT)
+ lines[i].position.x = (float)(object->x + object->width) - textSize.x;
+ else if (objectText->halign == HORIZONTAL_ALIGNMENT_CENTER)
+ lines[i].position.x = (float)(object->x + (object->width / 2.0)) - (textSize.x / 2.0f);
+ else if (objectText->halign == HORIZONTAL_ALIGNMENT_JUSTIFY) {
+ /* Horizontally justified text extends from the left bound to the right bound. Typically, */
+ /* this is done by adding space betweens words or, where there is only one word, adding */
+ /* space between letters. All additional space is distributed evenly. However, the method */
+ /* here is a hybrid: because control over spacing between words is coarse, spacing */
+ /* between letters is added on top of it. */
+ lines[i].position.x = (float)object->x; /* Place the text on the left bound */
+ /* Count the number of spaces between words */
+ uint32_t numSpaces = 0;
+ for (uint32_t j = 0; lines[i].content[j] != '\0'; j++) {
+ if (isspace(lines[i].content[j]))
+ numSpaces++;
+ }
+ size_t length = strlen(lines[i].content);
+ if (numSpaces > 0) { /* If there's more than one word in the line */
+ /* Measure the dimensions of a single space using this line's configuration */
+ Vector2 originalTextSize = textSize;
+ textSize = MeasureTextEx(font, " ", (float)objectText->pixelSize, lines[i].spacing);
+ /* Calculate the number of new spaces to add between words, per existing space */
+ float idealNumAdditionalSpaces =
+ ((float)object->width - originalTextSize.x) / textSize.x;
+ uint32_t numSpacesToAddPer =
+ (uint32_t)floor((idealNumAdditionalSpaces - (float)numSpaces) / (float)numSpaces);
+ /* Create a new string with the additional space */
+ size_t justifiedLength = length + (numSpacesToAddPer * numSpaces);
+ char* justifiedContent = (char*)MemAllocZero((unsigned int)justifiedLength + 1);
+ uint32_t sourceIndex = 0, destinationIndex = 0;
+ while (lines[i].content[sourceIndex] != '\0') {
+ justifiedContent[destinationIndex++] = lines[i].content[sourceIndex];
+ /* If the current character is whitespace but the next one is not */
+ if (sourceIndex < length && !isspace(lines[i].content[sourceIndex]) &&
+ isspace(lines[i].content[sourceIndex + 1])) {
+ /* Add 'numSpacesToAddPer' spaces to the justified string */
+ for (uint32_t j = 0; j < numSpacesToAddPer; j++)
+ justifiedContent[destinationIndex++] = ' ';
+ }
+ sourceIndex++;
+ }
+ /* Free the original content buffer and replace it with the justified one */
+ MemFree(lines[i].content);
+ lines[i].content = justifiedContent;
+ length = justifiedLength;
+ }
+ /* Calculate a spacing, between each letter, with which the drawn text will span the full */
+ /* width of the bounds */
+ textSize = MeasureTextEx(font, lines[i].content, (float)objectText->pixelSize, 0.0f);
+ lines[i].spacing = (float)((object->width - textSize.x) / (double)(length - 1));
+ } /* objectText->halign == HORIZONTAL_ALIGNMENT_JUSTIFY */
+ else /* if (objectText->halign == HORIZONTAL_ALIGNMENT_LEFT) */
+ lines[i].position.x = (float)object->x;
+
+ /* Vertical alignment */
+ if (objectText->valign == VERTICAL_ALIGNMENT_BOTTOM) {
+ lines[i].position.y = (float)(object->y + object->height) -
+ (float)(objectText->pixelSize * (i + 1));
+ } else if (objectText->valign == VERTICAL_ALIGNMENT_CENTER) {
+ float totalLineHeight = (float)(objectText->pixelSize * linesLength); /* All N lines */
+ lines[i].position.y = (float)object->y + /* Top of the <object>'s bounds */
+ ((float)object->height / 2.0f) + /* Vertical center of the <object>'s bounds */
+ (totalLineHeight / 2.0f) - /* Bottom of the centered lines' bounds */
+ (float)(objectText->pixelSize * (i + 1));
+ } else /* if (objectText->valign == VERTICAL_ALIGNMENT_TOP) */
+ lines[i].position.y = (float)object->y + (float)(objectText->pixelSize * i);
+
+ RaytmxTextLineNode* parent = iterator;
+ iterator = iterator->next;
+ MemFree(parent);
+ }
+ /* Add the lines array to the text object */
+ objectText->lines = lines;
+ objectText->linesLength = linesLength;
+ }
+ } /* objectText->content != NULL */
+ }
+ } /* strcmp(hoxmlContext->tag, "text") == 0 */
+ else if (strcmp(hoxmlContext->tag, "imagelayer") == 0) {
+ raytmxState->imageLayer = NULL;
+ raytmxState->layer = NULL;
+ } else if (strcmp(hoxmlContext->tag, "group") == 0) {
+ /* <group>s can be nested so we must return to processing its parent, if it exists */
+ if (raytmxState->groupNode != NULL)
+ raytmxState->groupNode = raytmxState->groupNode->parent; /* Will be null when returning to the root map */
+ }
+}
+
+void FreeStateLayers(RaytmxLayerNode* layers) {
+ RaytmxLayerNode *layersIterator = layers, *layersTemp;
+ while (layersIterator != NULL) {
+ /* Group layers may have children, forming a tree-like structure. Free the children. */
+ FreeStateLayers(layersIterator->childrenRoot);
+ /* Iterate to the next node and free this one */
+ layersTemp = layersIterator;
+ layersIterator = layersIterator->next;
+ MemFree(layersTemp);
+ }
+}
+
+void FreeState(RaytmxState* raytmxState) {
+ if (raytmxState == NULL)
+ return;
+
+ /* Clear the caches. These allow for quick lookups of previously-loaded textures and object templates. They */
+ /* aren't needed once loading is complete. */
+ RaytmxCachedTextureNode *cachedTextureIterator = raytmxState->texturesRoot, *cachedTextureTemp;
+ while (cachedTextureIterator != NULL) {
+ cachedTextureTemp = cachedTextureIterator;
+ cachedTextureIterator = cachedTextureIterator->next;
+ if (cachedTextureTemp->fileName != NULL) /* Just in case. Should always be set. */
+ MemFree(cachedTextureTemp->fileName);
+ MemFree(cachedTextureTemp);
+ }
+ raytmxState->texturesRoot = NULL;
+ RaytmxCachedTemplateNode *cachedTemplateIterator = raytmxState->templatesRoot, *cachedTemplateTemp;
+ while (cachedTemplateIterator != NULL) {
+ cachedTemplateTemp = cachedTemplateIterator;
+ cachedTemplateIterator = cachedTemplateIterator->next;
+ FreeObject(cachedTemplateTemp->objectTemplate.object);
+ if (cachedTemplateTemp->fileName != NULL) /* Just in case. Should always be set. */
+ MemFree(cachedTemplateTemp->fileName);
+ MemFree(cachedTemplateTemp);
+ }
+ raytmxState->templatesRoot = NULL;
+
+ raytmxState->property = NULL;
+ raytmxState->tileset = NULL;
+ raytmxState->image = NULL;
+ raytmxState->tilesetTile = NULL;
+ raytmxState->animationFrame = NULL;
+ /* raytmxState->wangSet = NULL; */ /* TODO: Wang sets. Low priority. */
+ /* raytmxState->wangColor = NULL; */ /* TODO: Wang sets. Low priority. */
+ raytmxState->layer = NULL;
+ raytmxState->tileLayer = NULL;
+ raytmxState->objectGroup = NULL;
+ raytmxState->imageLayer = NULL;
+ raytmxState->object = NULL;
+
+ /* Free each property in the linked list of properties */
+ RaytmxPropertyNode *propertiesIterator = raytmxState->propertiesRoot, *propertiesTemp;
+ while (propertiesIterator != NULL) {
+ propertiesTemp = propertiesIterator;
+ propertiesIterator = propertiesIterator->next;
+ MemFree(propertiesTemp);
+ }
+ /* Zeroize this linked list's properties */
+ raytmxState->propertiesRoot = NULL;
+ raytmxState->propertiesTail = NULL;
+ raytmxState->propertiesLength = 0;
+
+ /* Free each node in the linked list of tilesets */
+ RaytmxTilesetNode *tilesetsIterator = raytmxState->tilesetsRoot, *tilesetsTemp;
+ while (tilesetsIterator != NULL) {
+ tilesetsTemp = tilesetsIterator;
+ tilesetsIterator = tilesetsIterator->next;
+ MemFree(tilesetsTemp);
+ }
+ /* Zeroize this linked list's properties */
+ raytmxState->tilesetsRoot = NULL;
+ raytmxState->tilesetsTail = NULL;
+ raytmxState->tilesetsLength = 0;
+
+ /* Free each node in the linked list of tileset tiles */
+ RaytmxTilesetTileNode *tilesetTilesIterator = raytmxState->tilesetTilesRoot, *tilesetTilesTemp;
+ while (tilesetTilesIterator != NULL) {
+ tilesetTilesTemp = tilesetTilesIterator;
+ tilesetTilesIterator = tilesetTilesIterator->next;
+ MemFree(tilesetTilesTemp);
+ }
+ /* Zeroize this linked list's properties */
+ raytmxState->tilesetTilesRoot = NULL;
+ raytmxState->tilesetTilesTail = NULL;
+ raytmxState->tilesetTilesLength = 0;
+
+ /* Free each node in the linked list of animation frames */
+ RaytmxAnimationFrameNode *animationFramesIterator = raytmxState->animationFramesRoot, *animationFramesTemp;
+ while (animationFramesIterator != NULL) {
+ animationFramesTemp = animationFramesIterator;
+ animationFramesIterator = animationFramesIterator->next;
+ MemFree(animationFramesTemp);
+ }
+ /* Zeroize this linked list's properties */
+ raytmxState->animationFramesRoot = NULL;
+ raytmxState->animationFramesTail = NULL;
+ raytmxState->animationFramesLength = 0;
+
+ /* Layers may be groups. The resulting collection isn't a link list as much as it is a tree. So freeing layers is */
+ /* best done with a recursive approach. */
+ FreeStateLayers(raytmxState->layersRoot);
+ /* Zeroize this linked list's properties */
+ raytmxState->layersRoot = NULL;
+ raytmxState->layersTail = NULL;
+ raytmxState->layersLength = 0;
+
+ /* Free each node in the linked list of layer tiles */
+ RaytmxTileLayerTileNode *layerTilesIterator = raytmxState->layerTilesRoot, *layerTilesTemp;
+ while (layerTilesIterator != NULL) {
+ layerTilesTemp = layerTilesIterator;
+ layerTilesIterator = layerTilesIterator->next;
+ MemFree(layerTilesTemp);
+ }
+ /* Zeroize this linked list's properties */
+ raytmxState->layerTilesRoot = NULL;
+ raytmxState->layerTilesTail = NULL;
+ raytmxState->layerTilesLength = 0;
+
+ /* Free each node in the linked list of objects */
+ RaytmxObjectNode *objectsIterator = raytmxState->objectsRoot, *objectsTemp;
+ while (objectsIterator != NULL) {
+ objectsTemp = objectsIterator;
+ objectsIterator = objectsIterator->next;
+ MemFree(objectsTemp);
+ }
+ /* Zeroize this linked list's properties */
+ raytmxState->objectsRoot = NULL;
+ raytmxState->objectsTail = NULL;
+ raytmxState->objectsLength = 0;
+}
+
+void inline FreeString(char* str) {
+ if (str != NULL)
+ MemFree(str);
+}
+
+void FreeTileset(TmxTileset tileset) {
+ FreeString(tileset.source);
+ FreeString(tileset.name);
+ FreeString(tileset.classString);
+ if (tileset.hasImage) {
+ FreeString(tileset.image.source);
+ UnloadTexture(tileset.image.texture);
+ }
+ if (tileset.properties != NULL) {
+ for (uint32_t i = 0; i < tileset.propertiesLength; i++)
+ FreeProperty(tileset.properties[i]);
+ MemFree(tileset.properties);
+ }
+ for (uint32_t i = 0; i < tileset.tilesLength; i++) {
+ TmxTilesetTile tile = tileset.tiles[i];
+ FreeString(tile.classString);
+ if (tile.hasImage) {
+ FreeString(tile.image.source);
+ UnloadTexture(tile.image.texture);
+ if (tile.properties != NULL) {
+ for (uint32_t j = 0; j < tile.propertiesLength; j++)
+ FreeProperty(tile.properties[j]);
+ MemFree(tile.properties);
+ }
+ }
+ if (tile.animation.frames != NULL)
+ MemFree(tile.animation.frames);
+ }
+}
+
+void FreeProperty(TmxProperty property) {
+ FreeString(property.name);
+ FreeString(property.stringValue);
+}
+
+void FreeLayer(TmxLayer layer) {
+ FreeString(layer.name);
+ FreeString(layer.classString);
+ if (layer.properties != NULL) {
+ for (uint32_t i = 0; i < layer.propertiesLength; i++)
+ FreeProperty(layer.properties[i]);
+ MemFree(layer.properties);
+ }
+ switch (layer.type) {
+ case LAYER_TYPE_TILE_LAYER:
+ FreeString(layer.exact.tileLayer.encoding);
+ FreeString(layer.exact.tileLayer.compression);
+ MemFree(layer.exact.tileLayer.tiles);
+ break;
+ case LAYER_TYPE_OBJECT_GROUP:
+ for (uint32_t j = 0; j < layer.exact.objectGroup.objectsLength; j++)
+ FreeObject(layer.exact.objectGroup.objects[j]);
+ MemFree(layer.exact.objectGroup.objects);
+ break;
+ case LAYER_TYPE_IMAGE_LAYER:
+ if (layer.exact.imageLayer.hasImage)
+ UnloadTexture(layer.exact.imageLayer.image.texture);
+ break;
+ case LAYER_TYPE_GROUP: break; /* Nothing to do for this case but compilers like to complain */
+ }
+ /* <group> layers are expected to have child layers, or child <group>s, so recursively free them too */
+ for (uint32_t i = 0; i < layer.layersLength; i++)
+ FreeLayer(layer.layers[i]);
+}
+
+void FreeObject(TmxObject object) {
+ FreeString(object.name);
+ FreeString(object.typeString);
+ FreeString(object.templateString);
+ if (object.points != NULL)
+ MemFree(object.points);
+ if (object.text != NULL) {
+ if (object.text->lines != NULL) {
+ for (uint32_t j = 0; j < object.text->linesLength; j++)
+ FreeString(object.text->lines[j].content);
+ MemFree(object.text->lines);
+ } /* object.text->lines != NULL */
+ MemFree(object.text);
+ } /* object.text != NULL */
+}
+
+#define SIGN(x) (x < 0 ? -1 : +1)
+
+/**
+ * Helper function that keeps an integer within the given range.
+ * Note: A function named Clamp() exists in raylib but uses floats, hence Clampi().
+ *
+ * @param value The preferred value that will be used as long as it is between the minimum and maximum values.
+ * @param minimum The smallest acceptable value, inclusive.
+ * @param maximum The largest acceptable value, inclusive.
+ * @return The preferred value if within the range or the nearest acceptable value.
+ */
+int Clampi(int value, int minimum, int maximum) {
+ if (value < minimum)
+ return minimum;
+ else if (value > maximum)
+ return maximum;
+ return value;
+}
+
+/**
+ * Scary-looking helper function that does something kind of simple: iterates through the tiles of the given tile layer
+ * overlapping with the given viewport, one tile per call. This function returns true while iteration is still ongoing
+ * and false when done. This allows the function to be used e.g. "while (IterateTileLayer( { ...} ))". Details of the
+ * current tile are returned to the caller with output parameters. Iteration is done row-by-row.
+ *
+ * @param map A loaded map model containing the given tile layer.
+ * @param layer The tile layer within the given map whose tiles will be iterated.
+ * @param viewport A rectangle representing the region being drawn to. This could also be considered a search area.
+ * @param transform Position/offset numbers that affect where the tile is ultimately drawn.
+ * @param rawGid Optional output. The Global ID (GID) with possible flip flags. Pass NULL if not wanted.
+ * @param tile Optional output. Metadata of the current tile. Pass NULL if not wanted.
+ * @param tileRect Optional output. The destination rectangle, in pixels, of the current tile. Pass NULL if not wanted.
+ * @return True if the next tile is being provided via the output parameters, or false if iteration is done.
+ */
+bool IterateTileLayer(const TmxMap* map, const TmxTileLayer* layer, Rectangle viewport, RaytmxTransform transform,
+ uint32_t* rawGid, TmxTile* tile, Rectangle* tileRect) {
+ /* Static variables whose values will persist between calls. These are needed to initialize and iterate. */
+ static const TmxTileLayer* currentLayer = NULL; /* Tile layer being iterated */
+ static int fromX = 0; /* Initial X position, tile not pixel, that row-by-row iteration begins at */
+ static int fromY = 0; /* Initial Y position, tile not pixel, that row-by-row iteration begins at */
+ static int toX = 0; /* Final X position, tile not pixel, that iteration ends at */
+ static int toY = 0; /* Final Y position, tile not pixel, that iteration ends at */
+ static int currentX = 0; /* Current tile X position (column) within the iteration */
+ static int currentY = 0; /* Current tile Y position (row) within the iteration */
+
+ if (map == NULL || map->width == 0 || map->height == 0 || map->tileWidth == 0 || map->tileHeight == 0 ||
+ layer == NULL || layer->tilesLength == 0)
+ return false;
+
+ if (currentLayer != layer) { /* If the layer has changed (i.e. iteration should initialize) */
+ currentLayer = layer; /* Remember this layer */
+
+ /* Create an adjusted viewport that effectively removes the map's drawn position. With this, it doesn't need */
+ /* to be a factor in the math below. */
+ const Rectangle viewport2 = {
+ .x = viewport.x - transform.position.x,
+ .y = viewport.y - transform.position.y,
+ .width = viewport.width,
+ .height = viewport.height
+ };
+
+ switch (map->renderOrder) {
+ case RENDER_ORDER_RIGHT_DOWN:
+ /* Start at the top-left, iterate right, then iterate down, ending at the bottom-right. */
+ /* In other words, this is the order in which English is read. */
+ fromX = (int)viewport2.x / (int)map->tileWidth;
+ fromY = (int)viewport2.y / (int)map->tileHeight;
+ toX = (int)(viewport2.x + viewport2.width) / (int)map->tileWidth;
+ toY = (int)(viewport2.y + viewport2.height) / (int)map->tileHeight;
+ break;
+ case RENDER_ORDER_RIGHT_UP:
+ /* Start at the bottom-left, iterate right, then iterate up, ending at the top-right */
+ fromX = (int)viewport2.x / (int)map->tileWidth;
+ fromY = (int)(viewport2.y + viewport2.height) / (int)map->tileHeight;
+ toX = (int)(viewport2.x + viewport2.width) / (int)map->tileWidth;
+ toY = (int)viewport2.y / (int)map->tileHeight;
+ break;
+ case RENDER_ORDER_LEFT_DOWN:
+ /* Start at the top-right, iterate left, then iterate down, ending at the bottom-left */
+ fromX = (int)(viewport2.x + viewport2.width) / (int)map->tileWidth;
+ fromY = (int)viewport2.y / (int)map->tileHeight;
+ toX = (int)viewport2.x / (int)map->tileWidth;
+ toY = (int)(viewport2.y + viewport2.height) / (int)map->tileHeight;
+ break;
+ case RENDER_ORDER_LEFT_UP:
+ /* Start at the bottom-right, iterate left, then iterate up, ending at the top-left */
+ fromX = (int)(viewport2.x + viewport2.width) / (int)map->tileWidth;
+ fromY = (int)(viewport2.y + viewport2.height) / (int)map->tileHeight;
+ toX = (int)viewport2.x / (int)map->tileWidth;
+ toY = (int)viewport2.y / (int)map->tileHeight;
+ break;
+ } /* switch (map->renderOrder) */
+ /* Restrain the the tile positions to those within the map in case of rounding mistakes */
+ fromX = Clampi(fromX, 0, (int)map->width - 1);
+ fromY = Clampi(fromY, 0, (int)map->height - 1);
+ toX = Clampi(toX, 0, (int)map->width - 1);
+ toY = Clampi(toY, 0, (int)map->height - 1);
+ /* Begin iteration from both "from" tile positions */
+ currentX = fromX;
+ currentY = fromY;
+ } else if (currentX == toX) { /* If the end of the current row was reached */
+ /* Rendering is done row-by-row. This row is done so move to the next one. */
+ currentX = fromX;
+ currentY += SIGN(toY - fromY); /* Either +1 or -1 */
+ } else { /* If still iterating through the current row */
+ /* Move to the right or left by one tile */
+ currentX += SIGN(toX - fromX); /* Either +1 or -1 */
+ }
+
+ /* If iteration has gone beyond the final row */
+ if ((toY - fromY > 0 && currentY > toY) || (toY - fromY < 0 && currentY < toY)) {
+ /* This is the termination condition. Zero all values and return false so the caller exits its loop. */
+ currentLayer = NULL;
+ fromX = fromY = toX = toY = currentX = currentY = 0;
+ return false;
+ }
+
+ /* Calculate the index in the tile layer from knowing the tile's X and Y position (in tiles, not pixels) */
+ int index = (currentY * (int)map->width) + currentX;
+ if (index < 0 || index >= (int)layer->tilesLength) { /* Bounds check */
+ currentLayer = NULL;
+ fromX = fromY = toX = toY = currentX = currentY = 0;
+ return false;
+ }
+
+ /* Get the raw Global ID (GID) of the tile at this position from the layer's list of tiles. This list's order */
+ /* matches the map's render order. */
+ uint32_t localRawGid = layer->tiles[index];
+ if (rawGid != NULL)
+ *rawGid = localRawGid; /* Assign the value to he output parameter */
+ if (tile != NULL) {
+ /* The raw GID may have bit flags on it. They need to be removed in order to get the actual GID value.*/
+ uint32_t gid = GetGid(localRawGid, NULL, NULL, NULL, NULL);
+ /* Get the tile's metadata from knowing its GID */
+ if (gid < map->gidsToTilesLength) /* Bounds check */
+ *tile = map->gidsToTiles[gid];
+ }
+ if (tileRect != NULL) {
+ /* Calculate the tile's destination rectangle, in pixels */
+ *tileRect = (Rectangle) {
+ .x = (float)((uint32_t)currentX * map->tileWidth),
+ .y = (float)((uint32_t)currentY * map->tileHeight),
+ .width = (float)map->tileWidth,
+ .height = (float)map->tileHeight
+ };
+ }
+
+ return true;
+}
+
+void DrawTMXLayersInternal(const TmxMap* map, const Camera2D* camera, const Rectangle* viewport, const TmxLayer* layers,
+ uint32_t layersLength, RaytmxTransform transform, Color tint)
+{
+ if (map == NULL || layers == NULL || layersLength == 0)
+ return;
+
+ for (uint32_t i = 0; i < layersLength; i++) {
+ TmxLayer layer = layers[i];
+ if (!layers[i].visible) /* If the layer is not visible */
+ continue; /* Skip it - it's literally invisible */
+
+ /* All types of layers can have a couple attributes that affect color: 'opacity' and 'tintcolor' */
+ Color layerTint = tint;
+ layerTint.a = (unsigned char)((double)layerTint.a * layer.opacity);
+ if (layer.hasTintColor)
+ layerTint = ColorTint(layerTint, layer.tintColor);
+
+ /* Determine the viewport. This will depend on a couple parameters. If 'viewport' was assigned, it's used */
+ /* directly. If 'camera' was assigned, its target and zoom are used to derive a reasonable viewport from the */
+ /* screen's dimensions. If neither is assigned, the map's bounds are used. */
+ Rectangle viewport2;
+ if (viewport != NULL)
+ viewport2 = *viewport;
+ else if (camera != NULL) {
+ viewport2.width = (float)GetScreenWidth() / camera->zoom;
+ viewport2.height = (float)GetScreenHeight() / camera->zoom;
+ viewport2.x = camera->target.x - (viewport2.width / 2.0f);
+ viewport2.y = camera->target.y - (viewport2.height / 2.0f);
+ } else {
+ viewport2.x = transform.position.x;
+ viewport2.y = transform.position.y;
+ viewport2.width = (float)(map->width * map->tileWidth);
+ viewport2.height = (float)(map->height * map->tileHeight);
+ }
+
+ /* Create an updated transform for this layer. This will be the sum of the position the map is drawn at, the */
+ /* layer's offset, and the effect of parallax scrolling if conditions are met. */
+ RaytmxTransform transform2 = transform;
+ /* "The effective parallax scrolling factor of a layer is determined by multiplying the parallax scrolling */
+ /* factor by the scrolling factors of all parent layers." */
+ transform2.parallax.x *= (float)layer.parallaxX;
+ transform2.parallax.y *= (float)layer.parallaxY;
+ /* Apply the offset specific to this layer and/or parallax scrolling. Also applies to child layers. */
+ transform2.position.x += (float)layer.offsetX + (transform2.cameraOffset.x * (1.0f - transform2.parallax.x));
+ transform2.position.y += (float)layer.offsetY + (transform2.cameraOffset.y * (1.0f - transform2.parallax.y));
+
+ switch (layer.type) {
+ case LAYER_TYPE_TILE_LAYER: DrawTMXTileLayer(map, viewport2, layer, transform2, layerTint); break;
+ case LAYER_TYPE_OBJECT_GROUP: DrawTMXObjectGroup(map, viewport2, layer, transform2, layerTint); break;
+ case LAYER_TYPE_IMAGE_LAYER: DrawTMXImageLayer(map, viewport2, layer, transform2, layerTint); break;
+ case LAYER_TYPE_GROUP:
+ DrawTMXLayersInternal(map, camera, &viewport2, layer.layers, layer.layersLength, transform2, layerTint);
+ break;
+ }
+ }
+}
+
+void DrawTMXTileLayer(const TmxMap* map, Rectangle viewport, TmxLayer layer, RaytmxTransform transform, Color tint)
+{
+ if (map == NULL || layer.type != LAYER_TYPE_TILE_LAYER || layer.exact.tileLayer.tilesLength == 0)
+ return;
+
+ /* Iterate through each tile that the screen rectangle overlaps with */
+ uint32_t rawGid;
+ Rectangle tileRect;
+ while (IterateTileLayer(/* map: */ map, /* layer: */ &layer.exact.tileLayer, /* viewport: */ viewport,
+ /* transform: */ transform, /* rawGid: */ &rawGid, /* tile: */ NULL, /* tileRect: */ &tileRect))
+ {
+ tileRect.x += transform.position.x;
+ tileRect.y += transform.position.y;
+ DrawTMXLayerTile(/* map: */ map, /* viewport: */ viewport, /* rawGid: */ rawGid, /* destRect: */ tileRect,
+ /* tint: */ tint);
+ }
+}
+
+void DrawTextureTile(Texture2D texture, Rectangle source, Rectangle dest, bool flipX, bool flipY, bool flipDiag,
+ Color tint) {
+ if (texture.id == 0) /* If the texture is invalid */
+ return;
+
+ float textureWidth = (float)texture.width;
+ float textureHeight = (float)texture.height;
+
+ /* Determine the area within the texture to be drawn */
+ /* Note: The coordinates here are in the [0.0, 1.0] range where (0.0, 0.0) is the bottom-left corner of the */
+ /* texture, (1.0, 0.0) is the bottom-right, and (1.0, 1.0) is the top-right. In other words, the coordinates are */
+ /* a ratio of the dimensions making (0.5, 0.5) the center of the texture regardless of its aspect ratio. */
+ Vector2 sourceTopLeft, sourceTopRight, sourceBottomLeft, sourceBottomRight;
+ sourceTopLeft.x = source.x / textureWidth;
+ sourceTopLeft.y = source.y / textureHeight;
+ sourceTopRight.x = (source.x + source.width) / textureWidth;
+ sourceTopRight.y = source.y / textureHeight;
+ sourceBottomLeft.x = source.x / textureWidth;
+ sourceBottomLeft.y = (source.y + source.height) / textureHeight;
+ sourceBottomRight.x = (source.x + source.width) / textureWidth;
+ sourceBottomRight.y = (source.y + source.height) / textureHeight;
+ if (flipDiag) { /* If the tile uses a diagonal flip */
+ /* "The diagonal flip should flip the bottom left and top right corners of the tile..." */
+ Vector2 temp = sourceBottomLeft;
+ sourceBottomLeft = sourceTopRight;
+ sourceTopRight = temp;
+ }
+
+ /* Determine the area on the screen to be drawn to */
+ Vector2 destTopLeft, destTopRight, destBottomLeft, destBottomRight;
+ destTopLeft.x = dest.x;
+ destTopLeft.y = dest.y;
+ destTopRight.x = dest.x + dest.width;
+ destTopRight.y = dest.y;
+ destBottomLeft.x = dest.x;
+ destBottomLeft.y = dest.y + dest.height;
+ destBottomRight.x = dest.x + dest.width;
+ destBottomRight.y = dest.y + dest.height;
+
+ rlSetTexture(texture.id);
+ rlBegin(RL_QUADS);
+ {
+ rlColor4ub(tint.r, tint.g, tint.b, tint.a);
+ rlNormal3f(0.0f, 0.0f, 1.0f); /* Normal vector pointing towards viewer */
+
+ /* Top-left corner of the quad */
+ if (flipX && !flipY)
+ rlTexCoord2f(sourceTopRight.x, sourceTopRight.y);
+ else if (flipY && !flipX)
+ rlTexCoord2f(sourceBottomLeft.x, sourceBottomLeft.y);
+ else
+ rlTexCoord2f(sourceTopLeft.x, sourceTopLeft.y);
+ if (flipX && flipY)
+ rlVertex2f(destBottomRight.x, destBottomRight.y);
+ else
+ rlVertex2f(destTopLeft.x, destTopLeft.y);
+
+ /* Bottom-left corner of the quad */
+ if (flipX && !flipY)
+ rlTexCoord2f(sourceBottomRight.x, sourceBottomRight.y);
+ else if (flipY && !flipX)
+ rlTexCoord2f(sourceTopLeft.x, sourceTopLeft.y);
+ else
+ rlTexCoord2f(sourceBottomLeft.x, sourceBottomLeft.y);
+ if (flipX && flipY)
+ rlVertex2f(destTopRight.x, destTopRight.y);
+ else
+ rlVertex2f(destBottomLeft.x, destBottomLeft.y);
+
+ /* Bottom-right corner of the quad */
+ if (flipX && !flipY)
+ rlTexCoord2f(sourceBottomLeft.x, sourceBottomLeft.y);
+ else if (flipY && !flipX)
+ rlTexCoord2f(sourceTopRight.x, sourceTopRight.y);
+ else
+ rlTexCoord2f(sourceBottomRight.x, sourceBottomRight.y);
+ if (flipX && flipY)
+ rlVertex2f(destTopLeft.x, destTopLeft.y);
+ else
+ rlVertex2f(destBottomRight.x, destBottomRight.y);
+
+ /* Top-right corner of the quad */
+ if (flipX && !flipY)
+ rlTexCoord2f(sourceTopLeft.x, sourceTopLeft.y);
+ else if (flipY && !flipX)
+ rlTexCoord2f(sourceBottomRight.x, sourceBottomRight.y);
+ else
+ rlTexCoord2f(sourceTopRight.x, sourceTopRight.y);
+ if (flipX && flipY)
+ rlVertex2f(destBottomLeft.x, destBottomLeft.y);
+ else
+ rlVertex2f(destTopRight.x, destTopRight.y);
+ }
+ rlEnd();
+ rlSetTexture(0);
+}
+
+void DrawTMXLayerTile(const TmxMap* map, Rectangle viewport, uint32_t rawGid, Rectangle destRect, Color tint) {
+ if (map == NULL || tint.a == 0)
+ return;
+
+ bool isFlippedHorizontally, isFlippedVertically, isFlippedDiagonally, isRotatedHexagonal120;
+ /* Tile Global IDs (GIDs) can have several bit flags that indicate transforms. This function is used to both get */
+ /* those possible transform flags as well as the actual GID value without those bit flags. */
+ uint32_t gid = GetGid(rawGid, &isFlippedHorizontally, &isFlippedVertically, &isFlippedDiagonally,
+ &isRotatedHexagonal120);
+ if (gid >= map->gidsToTilesLength) /* If the GID is outside the range of known GIDs */
+ return; /* Do not attempt to draw this time */
+ /* With the GID, grab the relevant tile information (texture, animation, etc.) from the global mapping */
+ TmxTile tile = map->gidsToTiles[gid];
+ if (tile.gid == 0) /* If the GID is not known to exist in any tilesets within the map */
+ return; /* Do not attempt to draw this tile */
+ if (tile.hasAnimation && tile.frameIndex < tile.animation.framesLength &&
+ tile.animation.frames[tile.frameIndex].gid < map->gidsToTilesLength) { /* If the tile is/has an animation */
+ /* Animation tiles are meta; they contain a list of other GIDs to be drawn. Get the actual tile to draw this */
+ /* frame from that list. */
+ gid = tile.animation.frames[tile.frameIndex].gid;
+ if (gid < map->gidsToTilesLength)
+ tile = map->gidsToTiles[gid];
+ }
+
+ /* Determine where the tile will be drawn. raylib's coordinates consider (X, Y) to be the top-left corner of the */
+ /* rectangle being drawn. The TMX documentation complicates things a bit saying "Larger tiles will extend at the */
+ /* top and right (anchored to the bottom left)" meaning that TMX considers (X, Y) to be the bottom-left corner. */
+ /* The simplest way to reconcile the Y coordinate differences is to substract the texture's height at Y + 1. This */
+ /* way, tiles larger than the map's tile height values will be drawn further up (negative Y direction). */
+ destRect.x += tile.offset.x;
+ destRect.y += tile.offset.y + map->tileHeight - tile.sourceRect.height;
+
+ /* If the viewport and destination rectangles are overlapping to any degree (i.e. if the tile is visible) */
+ if (CheckCollisionRecs(viewport, destRect)) {
+ DrawTextureTile(/* texture: */ tile.texture, /* source: */ tile.sourceRect, /* dest: */ destRect,
+ /* flipX: */ isFlippedHorizontally, /* flipY: */ isFlippedVertically, /* flipDiag: */ isFlippedDiagonally,
+ /* tint: */ tint);
+ }
+}
+
+void DrawTMXObjectTile(const TmxMap* map, Rectangle viewport, uint32_t rawGid, RaytmxTransform transform, float width,
+ float height, Color tint) {
+ if (map == NULL || width <= 0 || height <= 0 || tint.a == 0)
+ return;
+
+ bool isFlippedHorizontally, isFlippedVertically, isFlippedDiagonally, isRotatedHexagonal120;
+ /* Tile Global IDs (GIDs) can have several bit flags that indicate transforms. This function is used to both get */
+ /* those possible transform flags as well as the actual GID value without those bit flags. */
+ uint32_t gid = GetGid(rawGid, &isFlippedHorizontally, &isFlippedVertically, &isFlippedDiagonally,
+ &isRotatedHexagonal120);
+ if (gid >= map->gidsToTilesLength) /* If the GID is outside the range of known GIDs */
+ return; /* Do not attempt to draw this time */
+ /* With the GID, grab the relevant tile information (texture, animation, etc.) from the global mapping */
+ TmxTile tile = map->gidsToTiles[gid];
+ if (tile.gid == 0) /* If the GID is not known to exist in any tilesets within the map */
+ return; /* Do not attempt to draw this time */
+ if (tile.hasAnimation && tile.frameIndex < tile.animation.framesLength &&
+ tile.animation.frames[tile.frameIndex].gid < map->gidsToTilesLength) { /* If the tile is/has an animation */
+ /* Animation tiles are meta; they contain a list of other GIDs to be drawn. Get the actual tile to draw this */
+ /* frame from that list. */
+ gid = tile.animation.frames[tile.frameIndex].gid;
+ if (gid < map->gidsToTilesLength)
+ tile = map->gidsToTiles[gid];
+ }
+
+ /* Determine the area in which to draw, and potentially stretch, the texture. This area matches that of the */
+ /* <object>, not the tile size. This also means that the Y coordinate needs consideration because raylib */
+ /* considers (X, Y) to the be top-left corner of any area but the TMX format considers it the bottom-left. */
+ const Rectangle destRect =
+ {
+ .x = transform.position.x + tile.offset.x,
+ .y = transform.position.y + tile.offset.y - height,
+ .width = width,
+ .height = height
+ };
+
+ /* If the viewport and destination rectangles are overlapping to any degree (i.e. if the tile is visible) */
+ if (CheckCollisionRecs(viewport, destRect)) {
+ DrawTextureTile(/* texture: */ tile.texture, /* source: */ tile.sourceRect, /* dest: */ destRect,
+ /* flipX: */ isFlippedHorizontally, /* flipY: */ isFlippedVertically, /* flipDiag: */ isFlippedDiagonally,
+ /* tint: */ tint);
+ }
+}
+
+void DrawTMXObjectGroup(const TmxMap* map, Rectangle viewport, TmxLayer layer, RaytmxTransform transform, Color tint) {
+ if (map == NULL || layer.type != LAYER_TYPE_OBJECT_GROUP || tint.a == 0)
+ return;
+
+ TmxObjectGroup objectGroup = layer.exact.objectGroup;
+ for (int32_t i = 0; i < (int32_t)objectGroup.objectsLength; i++) {
+ /* Select the object to draw based on the <objectgroup>'s draw order */
+ TmxObject object;
+ if (objectGroup.drawOrder == OBJECT_GROUP_DRAW_ORDER_INDEX)
+ object = objectGroup.objects[i];
+ else /* if (objectGroup.drawOrder == OBJECT_GROUP_DRAW_ORDER_TOP_DOWN) */
+ object = objectGroup.objects[objectGroup.ySortedObjects[i]];
+
+ if (object.type == OBJECT_TYPE_TILE) /* If the object is a tile with an abitrary GID and dimensions */
+ {
+ RaytmxTransform transform2 = transform;
+ transform2.position.x += (float)object.x;
+ transform2.position.y += (float)object.y;
+ /* Note: This draw method handles occlusion culling so it doesn't need to be done here */
+ DrawTMXObjectTile(map, /* viewport: */ viewport, /* rawGid: */ object.gid, /* transform: */ transform2,
+ /* width: */ (float)object.width, /* height: */ (float)object.height, /* color: */ tint);
+ }
+ else /* If the object is any type other than a tile */
+ {
+ Rectangle offsetAabb = object.aabb;
+ offsetAabb.x += transform.position.x;
+ offsetAabb.y += transform.position.y;
+ /* If the viewport and the polygon's AABB are overlapping to any degree (i.e. it is visible) */
+ if (CheckCollisionRecs(viewport, offsetAabb)) {
+ switch (object.type) {
+ case OBJECT_TYPE_RECTANGLE:
+ {
+ const Rectangle rectangle =
+ {
+ .x = transform.position.x + (float)object.x,
+ .y = transform.position.y + (float)object.y,
+ .width = (float)object.width,
+ .height = (float)object.height
+ };
+ DrawRectangleRec(/* rec: */ rectangle, /* color: */ objectGroup.color);
+ }
+ break;
+ case OBJECT_TYPE_ELLIPSE:
+ {
+ /* The width and height of the object determine the semi major and minor axes. That means the */
+ /* object's width is twice the horizontal radius and its height is twice the vertical radius. */
+ const float halfWidth = (float)object.width / 2.0f;
+ const float halfHeight = (float)object.height / 2.0f;
+ DrawEllipse(/* centerX: */ transform.position.x + (float)object.x + halfWidth,
+ /* centerY: */ transform.position.y + (float)object.y + halfHeight, /* radiusH: */ halfWidth,
+ /* radiusV: */ halfHeight, /* color: */ objectGroup.color);
+ }
+ break;
+ case OBJECT_TYPE_POINT:
+ {
+ /* A point is a single pixel but the TMX format doesn't require the point must be drawn any */
+ /* particular way. Tiled uses a pin icon. Drawing the exact pixel is tempting but hard to see so */
+ /* the point is drawn with a circle, with a diameter equal to a quarter of a tile's width. */
+ const Vector2 center =
+ {
+ .x = transform.position.x + (float)object.x,
+ .y = transform.position.y + (float)object.y
+ };
+ DrawCircleV(/* center: */ center, /* radius: */ (float)map->tileWidth / 4.0f,
+ /*color: */ objectGroup.color);
+ }
+ break;
+ case OBJECT_TYPE_POLYGON:
+ case OBJECT_TYPE_POLYLINE:
+ /* Copy the 'points' array to the 'drawPoints' array and apply the drawing position, an offset */
+ /* applied by the layer and/or draw call. The 'drawPoints' array was allocated at the same time */
+ /* as 'points' with the same size. This improves draw call times by reducing memory allocations. */
+ memcpy(object.drawPoints, object.points, sizeof(Vector2) * object.pointsLength);
+ for (uint32_t i = 0; i < object.pointsLength; i++) {
+ /* Polygons' and polyglines' vertices are stored with relative positions. To get the absolute */
+ /* position needed for drawing, just add the object's position and offset. */
+ object.drawPoints[i].x += transform.position.x + (float)object.x;
+ object.drawPoints[i].y += transform.position.y + (float)object.y;
+ }
+ /* Use the offset points to draw the poly(gon|line) */
+ if (object.type == OBJECT_TYPE_POLYGON) {
+ /* Note: Polygons' first elements are their centroids. DrawTriangleFan() requires this. */
+ /* And, the last element in 'drawPoints' is a duplicate of the first, non-centroid point. */
+ DrawTriangleFan(/* points: */ object.drawPoints, /* pointCount: */ object.pointsLength,
+ /* color: */ objectGroup.color);
+ } else /* if (object.type == OBJECT_TYPE_POLYLINE) */ {
+ /* Note: The last element in 'drawPoints' is a duplicate of the first point */
+ for (uint32_t i = 1; i < object.pointsLength; i++) {
+ DrawLineEx(/* startPos: */ object.drawPoints[i - 1], /* endPos: */ object.drawPoints[i],
+ /* thick: */ TMX_LINE_THICKNESS, /* color: */ objectGroup.color);
+ }
+ }
+ break;
+ case OBJECT_TYPE_TEXT:
+ for (uint32_t i = 0; i < object.text->linesLength; i++) {
+ Vector2 position = object.text->lines[i].position;
+ position.x += transform.position.x;
+ position.y += transform.position.y;
+ DrawTextEx(/* font: */ object.text->lines[i].font, /* text: */ object.text->lines[i].content,
+ /* position: */ position, /* fontSize: */ (float)object.text->pixelSize,
+ /* spacing: */ object.text->lines[i].spacing, /* tint: */ object.text->color);
+ }
+ break;
+ case OBJECT_TYPE_TILE:
+ /* Object tiles are handled in the 'if' case of this 'else' block because the use of an AABB for */
+ /* occlusion culling is not reliable for them */
+ break;
+ }
+ }
+ }
+ }
+}
+
+void DrawTMXImageLayer(const TmxMap* map, Rectangle viewport, TmxLayer layer, RaytmxTransform transform, Color tint) {
+ if (map == NULL || layer.type != LAYER_TYPE_IMAGE_LAYER || !layer.exact.imageLayer.hasImage ||
+ layer.exact.imageLayer.image.width == 0 || layer.exact.imageLayer.image.height == 0 || tint.a == 0)
+ return;
+
+ TmxImageLayer imageLayer = layer.exact.imageLayer;
+ /* Determine where the image of this image layer would be drawn, assuming no repetitions */
+ Rectangle imageRect =
+ {
+ .x = transform.position.x,
+ .y = transform.position.y,
+ .width = (float)imageLayer.image.width,
+ .height = (float)imageLayer.image.height
+ };
+
+ if (!imageLayer.repeatX && !imageLayer.repeatY && CheckCollisionRecs(viewport, imageRect)) /* If visible */
+ DrawTextureV(/* texture: */ imageLayer.image.texture, /* position: */ transform.position, /* tint: */ tint);
+ else if (imageLayer.repeatX || imageLayer.repeatY) { /* If the image might be drawn across a whole axis, or both */
+ /* Use integer division to determine the X and Y positions at which a the image would appear if it were */
+ /* repeated across the whole axis (i.e. if "Repeat X" and/or "Repeat Y" are enabled) */
+ int coefficientX = (int)(viewport.x - imageRect.x) / (int)imageRect.width;
+ int coefficientY = (int)(viewport.y - imageRect.y) / (int)imageRect.height;
+ float x0 = imageRect.x + (imageRect.width * (float)coefficientX); /* Initial X position */
+ float y0 = imageRect.y + (imageRect.height * (float)coefficientY); /* Initial Y position */
+
+ if (imageLayer.repeatX) /* If repeating along the X axis */
+ imageRect.x = x0; /* Move the image's representative rectangle to that X */
+ if (imageLayer.repeatY)
+ imageRect.y = y0;
+ if (CheckCollisionRecs(viewport, imageRect)) { /* If the repeating image would be visible in the viewport */
+ /* Take a step back on each axis that the image repeats on. This ensures we don't leave any empty space */
+ /* along the left and/or top edge of the viewport. */
+ if (imageLayer.repeatX)
+ x0 -= imageRect.width;
+ if (imageLayer.repeatY)
+ y0 -= imageRect.height;
+
+ /* Create some unchanging objects that the draw function will need */
+ Rectangle sourceRect; /* Region within the texture to be drawn. We'll use the whole texture. */
+ sourceRect.x = 0.0f;
+ sourceRect.y = 0.0f;
+ sourceRect.width = (float)imageLayer.image.width;
+ sourceRect.height = (float)imageLayer.image.height;
+ Vector2 origin; /* Reference point used for rotations. We're not rotating so we'll just use all zeroes. */
+ origin.x = 0.0f;
+ origin.y = 0.0f;
+
+ if (imageLayer.repeatX && imageLayer.repeatY) { /* If repeating on both axes */
+ /* Loop over both the X and Y axes to draw an array of repeated images */
+ for (float x = x0; x <= viewport.x + viewport.width; x += imageRect.width) {
+ for (float y = y0; y <= viewport.y + viewport.height; y += imageRect.height) {
+ imageRect.x = x;
+ imageRect.y = y;
+ DrawTexturePro(/* texture: */ imageLayer.image.texture, /* source: */ sourceRect,
+ /* dest: */ imageRect, /* origin: */ origin, /* rotation: */ 0.0f, /* tint: */ tint);
+ }
+ }
+ } else if (imageLayer.repeatX) { /* If repeating on just the X axis */
+ /* Loop over just the X axis to draw a horizontal line of repeated images */
+ for (float x = x0; x <= viewport.x + viewport.width; x += imageRect.width) {
+ imageRect.x = x;
+ DrawTexturePro(/* texture: */ imageLayer.image.texture, /* source: */ sourceRect,
+ /* dest: */ imageRect, /* origin: */ origin, /* rotation: */ 0.0f, /* tint: */ tint);
+ }
+ } else if (imageLayer.repeatY) { /* If repeating on just the Y axis */
+ /* Loop over just the Y axis to draw a vertical line of repeated images */
+ for (float y = y0; y <= viewport.y + viewport.height; y += imageRect.height) {
+ imageRect.y = y;
+ DrawTexturePro(/* texture: */ imageLayer.image.texture, /* source: */ sourceRect,
+ /* dest: */ imageRect, /* origin: */ origin, /* rotation: */ 0.0f, /* tint: */ tint);
+ }
+ }
+ }
+ }
+}
+
+/**
+ * Helper function for detecting collisions between a line and a polygon given the line's start and end points and an
+ * array and count of the polygon's vertices.
+ *
+ * @param startPos One of the two points forming the line.
+ * @param endPos The other point forming the line.
+ * @param polyPos Position, in pixels, of the polygon. Its points are relative to this position.
+ * @param points Array of vertices of the polygon.
+ * @param pointCount Number of verticies in the polygon and, therefore, length of the array of vertices.
+ * @return True if the line and polygon collide with one another, or false if there is no collision.
+ */
+bool CheckCollisionLinePoly(Vector2 startPos, Vector2 endPos, Vector2 polyPos, Vector2* points, int pointCount) {
+ if (pointCount < 3)
+ return false;
+
+ /* Cycle through each edge of the polygon */
+ int nextIndex = 0;
+ for (int currentIndex = 0; currentIndex < pointCount; currentIndex++) {
+ /* Get the next index. If the current index is the last, wrap around. */
+ nextIndex = currentIndex + 1;
+ if (nextIndex == pointCount)
+ nextIndex = 0;
+ /* Get the current and next points. These two points form an edge of the polygon. */
+ Vector2 currentPoint = points[currentIndex];
+ Vector2 nextPoint = points[nextIndex];
+ /* Check these edges for collisions. Note: The last parameter is unused, hence zero. */
+ if (CheckCollisionLines(/* startPos1: */ startPos, /* endPos1: */ endPos, /* startPos2: */ currentPoint,
+ /* endPos: */ nextPoint, /* collisionPoint: */ NULL))
+ return true;
+ }
+
+ return false;
+}
+
+/**
+ * Helper function for detecting collisions between two polygons given arrays and counts of their vertices.
+ *
+ * @param polyPos1 Position, in pixels, of the first polygon. Its points are relative to this position.
+ * @param points1 Array of vertices of the first polygon.
+ * @param pointCount1 Number of vertices in the first polygon and, therefore, length of the array of vertices.
+ * @param polyPos2 Position, in pixels, of the second polygon. Its points are relative to this position.
+ * @param points2 Array of vertices of the second polygon.
+ * @param pointCount2 Number of vertices in the second polygon and, therefore, length of the array of vertices.
+ * @return True if the given polygons collide with one another, or false if there is no collision.
+ */
+bool CheckCollisionPolyPoly(Vector2 polyPos1, Vector2* points1, int pointCount1, Vector2 polyPos2, Vector2* points2,
+ int pointCount2) {
+ /* If either vertex array is missing or does not contain enough vertices to define a polygon */
+ if (points1 == NULL || pointCount1 < 3 || points2 == NULL || pointCount2 < 3)
+ return false;
+
+ /* Cycle through each edge of polygon 1 */
+ int nextIndex = 0;
+ for (int currentIndex = 0; currentIndex < pointCount1; currentIndex++) {
+ /* Get the next index. If the current index is the last, wrap around. */
+ nextIndex = currentIndex + 1;
+ if (nextIndex == pointCount1)
+ nextIndex = 0;
+ /* Get the current and next points. These two points form an edge of polygon 1. */
+ Vector2 currentPoint = points1[currentIndex];
+ currentPoint.x += polyPos1.x;
+ currentPoint.y += polyPos1.y;
+ Vector2 nextPoint = points1[nextIndex];
+ nextPoint.x += polyPos1.x;
+ nextPoint.y += polyPos1.y;
+ /* Check this edge for collisions against edges of polygon 2 */
+ if (CheckCollisionLinePoly(currentPoint, nextPoint, polyPos2, points2, pointCount2))
+ return true;
+ }
+
+ /* Check if polygon 1 is fully inside polygon 2 */
+ if (CheckCollisionPointPoly(/* point: */ points1[0], /* points: */ points2, /* pointCount: */ pointCount2))
+ return true;
+
+ /* Check if polygon 2 is fully inside polygon 1 */
+ if (CheckCollisionPointPoly(/* point: */ points2[0], /* points: */ points1, /* pointCount: */ pointCount1))
+ return true;
+
+ return false;
+}
+
+RAYTMX_DEC bool CheckCollisionTMXObjects(TmxObject object1, TmxObject object2) {
+ /* Perform a quick collision check on the Axis-Aligned Bounding Boxes (AABB) before more accurate checks */
+ if (!CheckCollisionRecs(/* rec1: */ object1.aabb, /* rec2: */ object2.aabb))
+ return false; /* The AABBs do not collide so the objects cannot possibly collide */
+
+ switch (object1.type) {
+ case OBJECT_TYPE_RECTANGLE: /* Object 1's type */
+ case OBJECT_TYPE_ELLIPSE: /* Object 1's type (treated as a rectangle due to difficulty) */
+ case OBJECT_TYPE_TEXT: /* Object 1's type */
+ case OBJECT_TYPE_TILE: /* Object 1's type */
+ switch (object2.type) {
+ case OBJECT_TYPE_RECTANGLE: /* Object 2's type */
+ case OBJECT_TYPE_ELLIPSE: /* Object 2's type (treated as a rectangle due to difficulty) */
+ case OBJECT_TYPE_TEXT: /* Object 2's type */
+ case OBJECT_TYPE_TILE: /* Object 2's type */
+ /* The objects' shapes, as far as collisions are concerned, are identical to the AABB. We already */
+ /* determined the AABBs collide so these objects collide. */
+ return true;
+
+ case OBJECT_TYPE_POINT: /* Object 2's type */
+ return CheckCollisionPointRec(/* point: */ (Vector2){(float)object2.x, (float)object2.y},
+ /* rec: */ object1.aabb);
+
+ case OBJECT_TYPE_POLYGON: /* Object 2's type */
+ case OBJECT_TYPE_POLYLINE: /* Object 2's type */
+ {
+ /* A rectangle is a polygon. Create an array of points to treat it as a polygon keeping in mind polygon */
+ /* vertices are relative so the top-left corner is always (0, 0). */
+ Vector2 points[4];
+ points[0] = (Vector2){0.0f, 0.0f};
+ points[1] = (Vector2){(float)object1.width, 0.0f};
+ points[2] = (Vector2){(float)object1.width, (float)object1.height};
+ points[3] = (Vector2){0.0f, (float)object1.height};
+ return CheckCollisionPolyPoly(/* polyPos1: */ (Vector2){(float)object1.x, (float)object1.y},
+ /* points1: */ points, /* pointCount: */ 4,
+ /* polyPos2: */ (Vector2){(float)object2.x, (float)object2.y}, /* points2: */ object2.points,
+ /* pointCount2: */ object2.pointsLength);
+ }
+ }
+ break;
+
+ case OBJECT_TYPE_POINT: /* Object 1's type */
+ switch (object2.type) {
+ case OBJECT_TYPE_RECTANGLE: /* Object 2's type */
+ case OBJECT_TYPE_ELLIPSE: /* Object 2's type (treated as a rectangle due to difficulty) */
+ case OBJECT_TYPE_TEXT: /* Object 2's type */
+ case OBJECT_TYPE_TILE: /* Object 2's type */
+ return CheckCollisionPointRec(/* point: */ (Vector2){(float)object1.x, (float)object2.y},
+ /* rec: */ object2.aabb);
+
+ case OBJECT_TYPE_POINT: /* Object 2's type */
+ return object1.x == object2.x && object1.y == object2.y;
+
+ case OBJECT_TYPE_POLYGON: /* Object 2's type */
+ case OBJECT_TYPE_POLYLINE: /* Object 2's type */
+ return CheckCollisionPointPoly((Vector2){(float)object1.x, (float)object1.y}, object2.points,
+ object2.pointsLength);
+ }
+ break;
+
+ case OBJECT_TYPE_POLYGON: /* Object 1's type */
+ case OBJECT_TYPE_POLYLINE: /* Object 1's type */
+ switch (object2.type) {
+ case OBJECT_TYPE_RECTANGLE: /* Object 2's type */
+ case OBJECT_TYPE_ELLIPSE: /* Object 2's type (treated as a rectangle due to difficulty) */
+ case OBJECT_TYPE_TEXT: /* Object 2's type */
+ case OBJECT_TYPE_TILE: /* Object 2's type */
+ {
+ /* A rectangle is a polygon. Create an array of points to treat it as a polygon keeping in mind polygon */
+ /* vertices are relative so the top-left corner is always (0, 0). */
+ Vector2 points[4];
+ points[0] = (Vector2){0.0f, 0.0f};
+ points[1] = (Vector2){(float)object2.width, 0.0f};
+ points[2] = (Vector2){(float)object2.width, (float)object2.height};
+ points[3] = (Vector2){0.0f, (float)object2.height};
+ return CheckCollisionPolyPoly(/* polyPos1: */ (Vector2){(float)object1.x, (float)object1.y},
+ /* points1: */ object1.points, /* pointCount1: */ object1.pointsLength,
+ /* polyPos2: */ (Vector2){(float)object2.x, (float)object2.y}, /* points2: */ points,
+ /* pointCount2: */ 4);
+ }
+
+ case OBJECT_TYPE_POINT: /* Object 2's type */
+ return CheckCollisionPointPoly(/* point: */ (Vector2){(float)object2.x, (float)object2.y},
+ /* points: */ object1.points, /* pointCount: */ object1.pointsLength);
+
+ case OBJECT_TYPE_POLYGON: /* Object 2's type */
+ case OBJECT_TYPE_POLYLINE: /* Object 2's type */
+ return CheckCollisionPolyPoly(/* polyPos1: */ (Vector2){(float)object1.x, (float)object1.y},
+ /* points1: */ object1.points, /* pointCount1: */ object1.pointsLength,
+ /* polyPos2: */ (Vector2){(float)object2.x, (float)object2.y}, /* points2: */ object2.points,
+ /* pointCount2: */ object2.pointsLength);
+ }
+ break;
+ }
+
+ return false;
+}
+
+/**
+ * Helper function for applying a translation to a given object by the given deltas.
+ *
+ * @param object The object whose position(s) should be translated.
+ * @param dx The X delta, in pixels, to translate.
+ * @param dy The Y delta, in pixels, to translate.
+ * @return A translated copy of the given object.
+ */
+TmxObject TranslateObject(TmxObject object, float dx, float dy) {
+ /* Translate the position of the object by the different X and Y deltas */
+ object.x += (double)dx;
+ object.y += (double)dy;
+ /* Translate the Axis-Aligned Bounding Box (AABB) to match */
+ object.aabb.x += dx;
+ object.aabb.y += dy;
+ /* Note: Although polygons and polylines have a series of vertices, they are relative to the object. This means */
+ /* nothing more needs to be done. Translating the object's X and Y effectively translates each vertex. */
+ return object;
+}
+
+/**
+ * Helper function for checking for collisions between 1+ tile layers, or groups potentially containing 1+ tile layers,
+ * and an object of arbitrary type. These checks use the collision information associated with tiles as object groups.
+ *
+ * @param map A loaded map model containing the given layers.
+ * @param layers An array of select tile layers or group layers to be checked for collisions.
+ * @param layersLength Length of the given array of tile layers.
+ * @param object A TMX <object> to be checked for a collision.
+ * @param outputObject Output parameter assigned with the object in the tile layer that the given object collided with.
+ * NULL if not wanted.
+ * @return True if one of the given tile layers collides with the given object, or false if there is no collision.
+ */
+bool CheckCollisionTMXTileLayerObject(const TmxMap* map, const TmxLayer* layers, uint32_t layersLength,
+ TmxObject object, TmxObject* outputObject) {
+ if (map == NULL || layers == NULL || layersLength == 0)
+ return false;
+
+ /* Iterate through each layer and check their tiles for collisions with the given object */
+ for (uint32_t i = 0; i < layersLength; i++) {
+ if (layers[i].type == LAYER_TYPE_TILE_LAYER) { /* If the layer has tiles */
+ /* Iterate through each tile that the object's Axis-Aligned Bounding Box (AABB) overlaps with */
+ TmxTile tile;
+ Rectangle tileRect;
+ RaytmxTransform transform = { .position = {0.0f, 0.0f}, .parallax = {1.0f, 1.0f} };
+ while (IterateTileLayer(/* map: */ map, /* layer: */ &layers[i].exact.tileLayer,
+ /* viewport: */ object.aabb, /* transform: */ transform, /* rawGid: */ NULL, /* tile: */ &tile,
+ /* tileRect: */ &tileRect))
+ {
+ /* Iterate through each object associated with the tile */
+ for (uint32_t j = 0; j < tile.objectGroup.objectsLength; j++) {
+ /* This object, the tile's collision information, has a relative position so this object must be */
+ /* translated to the position of the tile as it would be drawn with the layer */
+ TmxObject positionedObject = TranslateObject(tile.objectGroup.objects[j], tileRect.x, tileRect.y);
+ /* If this tile's object collides with the given object */
+ if (CheckCollisionTMXObjects(positionedObject, object)) {
+ if (outputObject != NULL)
+ *outputObject = positionedObject;
+ return true; /* Found a collision. Exit now to save some CPU cycles. */
+ }
+ }
+ }
+ } else if (layers[i].type == LAYER_TYPE_GROUP) { /* If the layer contains other layers */
+ if (CheckCollisionTMXTileLayerObject(map, layers[i].layers, layers[i].layersLength, object, outputObject))
+ return true;
+ }
+ }
+
+ return false;
+}
+
+/**
+ * Helper function for checking for collisions between an object group and an object of arbitrary type.
+ *
+ * @param group The object group whose 0+ objects will be checked for collisions.
+ * @param object A TMX <object> to be checked for collision.
+ * @param outputObject Output parameter assigned with the object in the object group that the given object collided
+ * with. NULL if not wanted.
+ * @return True if an object in the object group collides with the given object, or false if there is no collision.
+ */
+bool CheckCollisionTMXObjectGroupObject(TmxObjectGroup group, TmxObject object, TmxObject* outputObject) {
+ for (size_t i = 0; i < group.objectsLength; i++) {
+ if (CheckCollisionTMXObjects(group.objects[i], object)) {
+ if (outputObject != NULL)
+ *outputObject = group.objects[i];
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void TraceLogTMXTilesets(int logLevel, TmxOrientation orientation, TmxTileset* tilesets, uint32_t tilesetsLength,
+ int numSpaces) {
+ for (uint32_t i = 0; i < tilesetsLength; i++) {
+ TmxTileset tileset = tilesets[i];
+ if (i == 0)
+ TraceLog(logLevel, "tilesets:");
+ TraceLog(logLevel, " \"%s\":", tileset.name);
+ TraceLog(logLevel, " first GID: %u", tileset.firstGid);
+ TraceLog(logLevel, " last GID: %u", tileset.lastGid);
+ if (tileset.source != NULL)
+ TraceLog(logLevel, " source: \"%s\"", tileset.source);
+ if (tileset.classString[0] != '\0')
+ TraceLog(logLevel, " class: \"%s\"", tileset.classString);
+ TraceLog(logLevel, " tile width: %u", tileset.tileWidth);
+ TraceLog(logLevel, " tile height: %u", tileset.tileHeight);
+ if (tileset.spacing != 0)
+ TraceLog(logLevel, " spacing: %u", tileset.spacing);
+ if (tileset.margin != 0)
+ TraceLog(logLevel, " margin: %u", tileset.margin);
+ TraceLog(logLevel, " tile count: %u", tileset.tileCount);
+ TraceLog(logLevel, " columns: %u", tileset.columns);
+ if (tileset.tileOffsetX != 0)
+ TraceLog(logLevel, " tile offset X: %d", tileset.tileOffsetX);
+ if (tileset.tileOffsetY != 0)
+ TraceLog(logLevel, " tile offset Y: %d", tileset.tileOffsetY);
+ if ((orientation == ORIENTATION_ORTHOGONAL && tileset.objectAlignment != OBJECT_ALIGNMENT_BOTTOM_LEFT) ||
+ (orientation == ORIENTATION_ISOMETRIC && tileset.objectAlignment != OBJECT_ALIGNMENT_BOTTOM)) {
+ switch (tileset.objectAlignment) {
+ case OBJECT_ALIGNMENT_UNSPECIFIED: TraceLog(logLevel, " object alignment: unspecified"); break;
+ case OBJECT_ALIGNMENT_TOP_LEFT: TraceLog(logLevel, " object alignment: top-left"); break;
+ case OBJECT_ALIGNMENT_TOP: TraceLog(logLevel, " object alignment: top"); break;
+ case OBJECT_ALIGNMENT_TOP_RIGHT: TraceLog(logLevel, " object alignment: top-right"); break;
+ case OBJECT_ALIGNMENT_LEFT: TraceLog(logLevel, " object alignment: left"); break;
+ case OBJECT_ALIGNMENT_CENTER: TraceLog(logLevel, " object alignment: center"); break;
+ case OBJECT_ALIGNMENT_RIGHT: TraceLog(logLevel, " object alignment: right"); break;
+ case OBJECT_ALIGNMENT_BOTTOM_LEFT: TraceLog(logLevel, " object alignment: bottom-left"); break;
+ case OBJECT_ALIGNMENT_BOTTOM: TraceLog(logLevel, " object alignment: bottom"); break;
+ case OBJECT_ALIGNMENT_BOTTOM_RIGHT: TraceLog(logLevel, " object alignment: bottom-right"); break;
+ }
+ }
+ if (tileset.hasImage) {
+ TraceLog(logLevel, " image:");
+ TraceLog(logLevel, " source: \"%s\"", tileset.image.source);
+ if (tileset.image.hasTrans)
+ TraceLog(logLevel, " trans: 0x%08X", tileset.image.trans);
+ if (tileset.image.width != 0)
+ TraceLog(logLevel, " width: %u", tileset.image.width);
+ if (tileset.image.height != 0)
+ TraceLog(logLevel, " height: %u", tileset.image.height);
+ TraceLog(logLevel, " texture (ID): %u", tileset.image.texture.id);
+ }
+ for (uint32_t j = 0; j < tileset.tilesLength; j++) {
+ TmxTilesetTile tile = tileset.tiles[j];
+ if (j == 0)
+ TraceLog(logLevel, " tiles:");
+ TraceLog(logLevel, " ID: %u", tile.id);
+ if (tile.hasAnimation) {
+ for (uint32_t i = 0; i < tile.animation.framesLength; i++) {
+ if (i == 0)
+ TraceLog(logLevel, " frames:");
+ TraceLog(logLevel, " (G)ID: %u", tile.animation.frames[i].gid);
+ if (tileset.classString[0] != '\0')
+ TraceLog(logLevel, " class: \"%s\"", tileset.classString);
+ TraceLog(logLevel, " duration: %f", tile.animation.frames[i].duration);
+ }
+ }
+ TraceLogTMXProperties(logLevel, tile.properties, tile.propertiesLength, 8);
+ if (tile.hasImage && tile.image.texture.id != tileset.image.texture.id) {
+ /* The 'x,' 'y,' 'width,' and 'height' attributes relate to the image so only log them if one exists */
+ if (tile.x != 0)
+ TraceLog(logLevel, " x: %d", tile.x);
+ if (tile.y != 0)
+ TraceLog(logLevel, " y: %d", tile.y);
+ if (tile.width != tile.image.width)
+ TraceLog(logLevel, " width: %u", tile.width);
+ if (tile.height != tile.image.height)
+ TraceLog(logLevel, " height: %u", tile.height);
+ TraceLog(logLevel, " image:");
+ TraceLog(logLevel, " source: \"%s\"", tile.image.source);
+ if (tile.image.hasTrans)
+ TraceLog(logLevel, " trans: 0x%08X", tile.image.trans);
+ if (tile.image.width != 0)
+ TraceLog(logLevel, " width: %u", tile.image.width);
+ if (tile.image.height != 0)
+ TraceLog(logLevel, " height: %u", tile.image.height);
+ TraceLog(logLevel, " texture (ID): %u", tile.image.texture.id);
+ }
+ if (tile.objectGroup.objectsLength > 0) {
+ if (tmxLogFlags & LOG_SKIP_OBJECTS)
+ TraceLog(logLevel, " skipping %u objects", tile.objectGroup.objectsLength);
+ else {
+ TraceLog(logLevel, " objects:");
+ for (uint32_t k = 0; k < tile.objectGroup.objectsLength; k++)
+ TraceLogTMXObject(logLevel, tile.objectGroup.objects[k], numSpaces + 2);
+ }
+ }
+ }
+ TraceLogTMXProperties(logLevel, tileset.properties, tileset.propertiesLength, 2);
+ }
+}
+
+void TraceLogTMXProperties(int logLevel, TmxProperty* properties, uint32_t propertiesLength, int numSpaces) {
+ char padding[16];
+ memset(padding, '\0', 16);
+ StringCopyN(padding, " ", numSpaces);
+
+ if (tmxLogFlags & LOG_SKIP_PROPERTIES)
+ TraceLog(logLevel, "%sskipped %u properties", padding, propertiesLength);
+ else {
+ for (uint32_t i = 0; i < propertiesLength; i++) {
+ TmxProperty property = properties[i];
+ if (i == 0)
+ TraceLog(logLevel, "%sproperties:", padding);
+ switch (property.type) {
+ case PROPERTY_TYPE_STRING:
+ case PROPERTY_TYPE_FILE:
+ TraceLog(logLevel, "%s \"%s\": \"%s\"", padding, property.name, property.stringValue);
+ break;
+ case PROPERTY_TYPE_INT:
+ case PROPERTY_TYPE_OBJECT:
+ TraceLog(logLevel, "%s \"%s\": %d", padding, property.name, property.intValue);
+ break;
+ case PROPERTY_TYPE_FLOAT:
+ TraceLog(logLevel, "%s \"%s\": %f", padding, property.name, property.floatValue);
+ break;
+ case PROPERTY_TYPE_BOOL:
+ TraceLog(logLevel, "%s \"%s\": %s", padding, property.name, property.boolValue ? "true" : "false");
+ break;
+ case PROPERTY_TYPE_COLOR:
+ TraceLog(logLevel, "%s \"%s\": 0x%08X", padding, property.name, property.colorValue);
+ break;
+ }
+ }
+ }
+}
+
+void TraceLogTMXLayers(int logLevel, TmxLayer* layers, uint32_t layersLength, int numSpaces) {
+ char padding[16];
+ memset(padding, '\0', 16);
+ StringCopyN(padding, " ", numSpaces);
+
+ if (tmxLogFlags & LOG_SKIP_LAYERS)
+ TraceLog(logLevel, "%sskipped %u layers", padding, layersLength);
+ else {
+ uint32_t numTileLayers = 0, numObjectGroups = 0, numImageLayers = 0;
+ for (uint32_t i = 0; i < layersLength; i++) {
+ TmxLayer layer = layers[i];
+ if (i == 0)
+ TraceLog(logLevel, "%slayers:", padding);
+ /* If, based on the layer type, this layer isn't one that should be skipped */
+ if ((layer.type == LAYER_TYPE_GROUP) ||
+ (layer.type == LAYER_TYPE_TILE_LAYER && !(tmxLogFlags & LOG_SKIP_TILE_LAYERS)) ||
+ (layer.type == LAYER_TYPE_OBJECT_GROUP && !(tmxLogFlags & LOG_SKIP_OBJECT_GROUPS)) ||
+ (layer.type == LAYER_TYPE_IMAGE_LAYER && !(tmxLogFlags & LOG_SKIP_IMAGE_LAYERS))) {
+ /* Log the attributes of this layer common to all layers */
+ TraceLog(logLevel, "%s \"%s\":", padding, layer.name);
+ switch (layer.type) {
+ case LAYER_TYPE_TILE_LAYER: TraceLog(logLevel, "%s type: tile layer", padding); break;
+ case LAYER_TYPE_OBJECT_GROUP: TraceLog(logLevel, "%s type: object layer", padding); break;
+ case LAYER_TYPE_IMAGE_LAYER: TraceLog(logLevel, "%s type: image layer", padding); break;
+ case LAYER_TYPE_GROUP: TraceLog(logLevel, "%s type: group", padding); break;
+ }
+ if (layer.id > 0)
+ TraceLog(logLevel, "%s ID: %u", padding, layer.id);
+ if (layer.visible == false)
+ TraceLog(logLevel, "%s visible: false", padding);
+ if (layer.classString[0] != '\0')
+ TraceLog(logLevel, "%s class: \"%s\"", padding, layer.classString);
+ if (layer.offsetX != 0)
+ TraceLog(logLevel, "%s offset X: %d", padding, layer.offsetX);
+ if (layer.offsetY != 0)
+ TraceLog(logLevel, "%s offset Y: %d", padding, layer.offsetY);
+ if (layer.parallaxX != 1.0)
+ TraceLog(logLevel, "%s parallax X: %f", padding, layer.parallaxX);
+ if (layer.parallaxY != 1.0)
+ TraceLog(logLevel, "%s parallax Y: %f", padding, layer.parallaxY);
+ if (layer.opacity != 1.0)
+ TraceLog(logLevel, "%s opacity: %f", padding, layer.opacity);
+ if (layer.hasTintColor)
+ TraceLog(logLevel, "%s tint color: 0x%08X", padding, layer.tintColor);
+ TraceLogTMXProperties(logLevel, layer.properties, layer.propertiesLength, numSpaces + 4);
+ }
+
+ /* Log attributes specific to the layer's type (tile layer, object layer, image layer, or group) */
+ switch (layer.type) {
+ case LAYER_TYPE_TILE_LAYER:
+ numTileLayers += 1;
+ if (tmxLogFlags & LOG_SKIP_TILE_LAYERS)
+ continue;
+ if (layer.exact.tileLayer.width != 0)
+ TraceLog(logLevel, "%s width: %u", padding, layer.exact.tileLayer.width);
+ if (layer.exact.tileLayer.height != 0)
+ TraceLog(logLevel, "%s height: %u", padding, layer.exact.tileLayer.height);
+ if (tmxLogFlags & LOG_SKIP_TILES)
+ TraceLog(logLevel, "%s skipping %u tiles", padding, layer.exact.tileLayer.tilesLength);
+ else {
+ for (uint32_t j = 0; j < layer.exact.tileLayer.tilesLength; j++) {
+ if (j == 0)
+ TraceLog(logLevel, "%s tiles:", padding);
+ TraceLog(logLevel, "%s GID: %u", padding, layer.exact.tileLayer.tiles[j]);
+ }
+ }
+ break;
+ case LAYER_TYPE_OBJECT_GROUP:
+ numObjectGroups += 1;
+ if (tmxLogFlags & LOG_SKIP_OBJECT_GROUPS)
+ continue;
+ if (layer.exact.objectGroup.hasColor)
+ TraceLog(logLevel, "%s color: 0x%08X", padding, layer.exact.objectGroup.color);
+ /* if (layer.exact.objectGroup.width != 0)
+ TraceLog(logLevel, "%s width: %u", padding, layer.exact.objectGroup.width); */
+ /* if (layer.exact.objectGroup.height != 0)
+ TraceLog(logLevel, "%s height: %u", padding, layer.exact.objectGroup.height); */
+ if (layer.exact.objectGroup.drawOrder != OBJECT_GROUP_DRAW_ORDER_TOP_DOWN) {
+ switch (layer.exact.objectGroup.drawOrder) {
+ case OBJECT_GROUP_DRAW_ORDER_TOP_DOWN:
+ default:
+ TraceLog(logLevel, "%s draw order: top-down", padding);
+ break;
+ case OBJECT_GROUP_DRAW_ORDER_INDEX:
+ TraceLog(logLevel, "%s draw order: index", padding);
+ break;
+ }
+ }
+ if (tmxLogFlags & LOG_SKIP_OBJECTS)
+ TraceLog(logLevel, "%s skipping %u objects", padding, layer.exact.objectGroup.objectsLength);
+ else {
+ for (uint32_t j = 0; j < layer.exact.objectGroup.objectsLength; j++) {
+ TmxObject object = layer.exact.objectGroup.objects[j];
+ if (j == 0)
+ TraceLog(logLevel, "%s objects:", padding);
+ TraceLogTMXObject(logLevel, object, numSpaces);
+ }
+ }
+ break;
+ case LAYER_TYPE_IMAGE_LAYER:
+ numImageLayers += 1;
+ if (tmxLogFlags & LOG_SKIP_IMAGE_LAYERS)
+ continue;
+ if (layer.exact.imageLayer.repeatX)
+ TraceLog(logLevel, "%s repeat X: true", padding);
+ if (layer.exact.imageLayer.repeatY)
+ TraceLog(logLevel, "%s repeat Y: true", padding);
+ if (layer.exact.imageLayer.hasImage) {
+ TraceLog(logLevel, "%s image:", padding);
+ TraceLog(logLevel, "%s source: \"%s\"", padding, layer.exact.imageLayer.image.source);
+ if (layer.exact.imageLayer.image.hasTrans)
+ TraceLog(logLevel, "%s trans: 0x%08X", padding, layer.exact.imageLayer.image.trans);
+ if (layer.exact.imageLayer.image.width != 0)
+ TraceLog(logLevel, "%s width: %u", padding, layer.exact.imageLayer.image.width);
+ if (layer.exact.imageLayer.image.height != 0)
+ TraceLog(logLevel, "%s height: %u", padding, layer.exact.imageLayer.image.height);
+ TraceLog(logLevel, "%s texture (ID): %u", padding, layer.exact.imageLayer.image.texture.id);
+ }
+ break;
+ case LAYER_TYPE_GROUP:
+ TraceLogTMXLayers(logLevel, layer.layers, layer.layersLength, numSpaces + 4);
+ break;
+ }
+ }
+ if (tmxLogFlags & LOG_SKIP_TILE_LAYERS && numTileLayers > 0)
+ TraceLog(logLevel, "%s skipped %u tile layers", padding, numTileLayers);
+ if (tmxLogFlags & LOG_SKIP_OBJECT_GROUPS && numObjectGroups > 0)
+ TraceLog(logLevel, "%s skipped %u object layers", padding, numObjectGroups);
+ if (tmxLogFlags & LOG_SKIP_IMAGE_LAYERS && numImageLayers > 0)
+ TraceLog(logLevel, "%s skipped %u image layers", padding, numImageLayers);
+ }
+}
+
+void TraceLogTMXObject(int logLevel, TmxObject object, int numSpaces) {
+ char padding[16];
+ memset(padding, '\0', 16);
+ StringCopyN(padding, " ", numSpaces);
+
+ TraceLog(logLevel, "%s ID: %u", padding, object.id);
+ TraceLog(logLevel, "%s name: \"%s\"", padding, object.name);
+ switch (object.type) {
+ case OBJECT_TYPE_RECTANGLE: TraceLog(logLevel, "%s type: quad", padding); break;
+ case OBJECT_TYPE_ELLIPSE: TraceLog(logLevel, "%s type: ellipse", padding); break;
+ case OBJECT_TYPE_POINT: TraceLog(logLevel, "%s type: point", padding); break;
+ case OBJECT_TYPE_POLYGON: TraceLog(logLevel, "%s type: polygon", padding); break;
+ case OBJECT_TYPE_POLYLINE: TraceLog(logLevel, "%s type: polyline", padding); break;
+ case OBJECT_TYPE_TEXT: TraceLog(logLevel, "%s type: text", padding); break;
+ case OBJECT_TYPE_TILE: TraceLog(logLevel, "%s type: tile", padding); break;
+ }
+ if (object.typeString[0] != '\0')
+ TraceLog(logLevel, "%s type: \"%s\"", padding, object.typeString);
+ if (object.x != 0.0)
+ TraceLog(logLevel, "%s x: %f", padding, object.x);
+ if (object.y != 0.0)
+ TraceLog(logLevel, "%s y: %f", padding, object.y);
+ if (object.width != 0.0)
+ TraceLog(logLevel, "%s width: %f", padding, object.width);
+ if (object.height != 0.0)
+ TraceLog(logLevel, "%s height: %f", padding, object.height);
+ if (object.rotation != 0.0)
+ TraceLog(logLevel, "%s rotation: %f", padding, object.rotation);
+ if (object.gid > 0)
+ TraceLog(logLevel, "%s GID: %u", padding, object.gid);
+ if (!object.visible)
+ TraceLog(logLevel, "%s visible: false", padding);
+ if (object.templateString != NULL)
+ TraceLog(logLevel, "%s template: \"%s\"", padding, object.templateString);
+ for (uint32_t k = 0; k < object.pointsLength; k++) {
+ if (k == 0)
+ TraceLog(logLevel, "%s points:", padding);
+ TraceLog(logLevel, "%s [%f, %f]", padding, object.points[k].x, object.points[k].y);
+ }
+ TraceLogTMXProperties(logLevel, object.properties, object.propertiesLength, numSpaces + 8);
+ if (object.text != NULL) {
+ TraceLog(logLevel, "%s font family: \"%s\"", padding, object.text->fontFamily);
+ TraceLog(logLevel, "%s pixel size: %u", padding, object.text->pixelSize);
+ if (object.text->wrap)
+ TraceLog(logLevel, "%s wrap: true", padding);
+ TraceLog(logLevel, "%s color: 0x%08X", padding, object.text->color);
+ if (object.text->bold)
+ TraceLog(logLevel, "%s bold: true", padding);
+ if (object.text->italic)
+ TraceLog(logLevel, "%s italic: true", padding);
+ if (object.text->underline)
+ TraceLog(logLevel, "%s underline: true", padding);
+ if (object.text->strikeOut)
+ TraceLog(logLevel, "%s strike out: true", padding);
+ if (object.text->kerning)
+ TraceLog(logLevel, "%s kerning: true", padding);
+ switch (object.text->halign) {
+ case HORIZONTAL_ALIGNMENT_LEFT:
+ TraceLog(logLevel, "%s horizontal align: left", padding);
+ break;
+ case HORIZONTAL_ALIGNMENT_CENTER:
+ TraceLog(logLevel, "%s horizontal align: center", padding);
+ break;
+ case HORIZONTAL_ALIGNMENT_RIGHT:
+ TraceLog(logLevel, "%s horizontal align: right", padding);
+ break;
+ case HORIZONTAL_ALIGNMENT_JUSTIFY:
+ TraceLog(logLevel, "%s horizontal align: justify", padding);
+ break;
+ }
+ switch (object.text->valign) {
+ case VERTICAL_ALIGNMENT_TOP:
+ TraceLog(logLevel, "%s vertical align: top", padding);
+ break;
+ case VERTICAL_ALIGNMENT_CENTER:
+ TraceLog(logLevel, "%s vertical align: center", padding);
+ break;
+ case VERTICAL_ALIGNMENT_BOTTOM:
+ TraceLog(logLevel, "%s vertical align: bottom", padding);
+ break;
+ }
+ if (object.text->content[0] != '\0')
+ TraceLog(logLevel, "%s content: \"%s\"", padding, object.text->content);
+ }
+}
+
+TmxProperty* AddProperty(RaytmxState* raytmxState) {
+ RaytmxPropertyNode* node = (RaytmxPropertyNode*)MemAllocZero(sizeof(RaytmxPropertyNode));
+
+ if (raytmxState->propertiesRoot == NULL)
+ raytmxState->propertiesRoot = node;
+ else
+ raytmxState->propertiesTail->next = node;
+ raytmxState->propertiesTail = node;
+ raytmxState->propertiesLength += 1;
+
+ return &node->property;
+}
+
+void AddTileLayerTile(RaytmxState* raytmxState, uint32_t gid) {
+ RaytmxTileLayerTileNode* node = (RaytmxTileLayerTileNode*)MemAllocZero(sizeof(RaytmxTileLayerTileNode));
+ node->gid = gid;
+
+ if (raytmxState->layerTilesRoot == NULL)
+ raytmxState->layerTilesRoot = node;
+ else
+ raytmxState->layerTilesTail->next = node;
+ raytmxState->layerTilesTail = node;
+ raytmxState->layerTilesLength += 1;
+}
+
+TmxTileset* AddTileset(RaytmxState* raytmxState) {
+ RaytmxTilesetNode* node = (RaytmxTilesetNode*)MemAllocZero(sizeof(RaytmxTilesetNode));
+
+ if (raytmxState->tilesetsRoot == NULL)
+ raytmxState->tilesetsRoot = node;
+ else
+ raytmxState->tilesetsTail->next = node;
+ raytmxState->tilesetsTail = node;
+ raytmxState->tilesetsLength += 1;
+
+ return &node->tileset;
+}
+
+TmxTilesetTile* AddTilesetTile(RaytmxState* raytmxState) {
+ RaytmxTilesetTileNode* node = (RaytmxTilesetTileNode*)MemAllocZero(sizeof(RaytmxTilesetTileNode));
+
+ if (raytmxState->tilesetTilesRoot == NULL)
+ raytmxState->tilesetTilesRoot = node;
+ else
+ raytmxState->tilesetTilesTail->next = node;
+ raytmxState->tilesetTilesTail = node;
+ raytmxState->tilesetTilesLength += 1;
+
+ return &node->tile;
+}
+
+TmxAnimationFrame* AddAnimationFrame(RaytmxState* raytmxState) {
+ RaytmxAnimationFrameNode* node = (RaytmxAnimationFrameNode*)MemAllocZero(sizeof(RaytmxAnimationFrameNode));
+
+ if (raytmxState->animationFramesRoot == NULL)
+ raytmxState->animationFramesRoot = node;
+ else
+ raytmxState->animationFramesTail->next = node;
+ raytmxState->animationFramesTail = node;
+ raytmxState->animationFramesLength += 1;
+
+ return &node->frame;
+}
+
+TmxLayer* AddGenericLayer(RaytmxState* raytmxState, bool isGroup) {
+ RaytmxLayerNode* node = (RaytmxLayerNode*)MemAllocZero(sizeof(RaytmxLayerNode));
+ /* There are some non-zero default values for several layer attributes: */
+ node->layer.opacity = 1.0;
+ node->layer.visible = true;
+ node->layer.parallaxX = 1.0;
+ node->layer.parallaxY = 1.0;
+
+ if (raytmxState->groupNode != NULL) { /* If the layer is being appended to a <group> */
+ node->parent = raytmxState->groupNode;
+ if (raytmxState->groupNode->childrenRoot == NULL) /* If the layer is the first in the group */
+ raytmxState->groupNode->childrenRoot = node;
+ else
+ raytmxState->groupNode->childrenTail->next = node;
+ raytmxState->groupNode->childrenTail = node;
+ raytmxState->groupNode->childrenLength += 1;
+ } else { /* If there is no <group> to append to, meaning the layer belongs to the top-level <map> */
+ if (raytmxState->layersRoot == NULL) /* If the layer is the first in the map */
+ raytmxState->layersRoot = node;
+ else
+ raytmxState->layersTail->next = node;
+ raytmxState->layersTail = node;
+ raytmxState->layersLength += 1;
+ }
+
+ if (isGroup)
+ raytmxState->groupNode = node;
+
+ return &node->layer;
+}
+
+TmxObject* AddObject(RaytmxState* raytmxState) {
+ RaytmxObjectNode* node = (RaytmxObjectNode*)MemAllocZero(sizeof(RaytmxObjectNode));
+ /* <object> elements have one non-zero default value: */
+ node->object.visible = true;
+
+ if (raytmxState->objectsRoot == NULL)
+ raytmxState->objectsRoot = node;
+ else
+ raytmxState->objectsTail->next = node;
+ raytmxState->objectsTail = node;
+ raytmxState->objectsLength += 1;
+
+ return &node->object;
+}
+
+void AppendLayerTo(TmxMap* map, RaytmxLayerNode* groupNode, RaytmxLayerNode* layersRoot, uint32_t layersLength) {
+ if (map == NULL || layersRoot == NULL || layersLength == 0)
+ return;
+
+ TmxLayer* groupLayer = NULL;
+ if (groupNode != NULL)
+ groupLayer = &(groupNode->layer);
+
+ /* Allocate the array and zerioze every index as initialization */
+ TmxLayer* layers = (TmxLayer*)MemAllocZero(sizeof(TmxLayer) * layersLength);
+ /* Copy the TmxLayers into the array */
+ RaytmxLayerNode* layersIterator = layersRoot;
+ for (uint32_t i = 0; layersIterator != NULL; i++) {
+ if (layersIterator->childrenRoot != NULL)
+ AppendLayerTo(map, layersIterator, layersIterator->childrenRoot, layersIterator->childrenLength);
+ layers[i] = layersIterator->layer;
+ layersIterator = layersIterator->next;
+ }
+
+ if (groupLayer != NULL) { /* If the list of layers is being appended to a group */
+ groupLayer->layers = layers;
+ groupLayer->layersLength = layersLength;
+ } else { /* If the list of layers is being appended to the top-level map */
+ map->layers = layers;
+ map->layersLength = layersLength;
+ }
+}
+
+RaytmxCachedTextureNode* LoadCachedTexture(RaytmxState* raytmxState, const char* fileName) {
+ if (raytmxState == NULL || fileName == NULL)
+ return NULL;
+
+ /* First try to find an already-loaded texture identified by the file name */
+ RaytmxCachedTextureNode* cachedTextureNode = raytmxState->texturesRoot;
+ while (cachedTextureNode != NULL) {
+ /* If the file name associated with the node matches the given file name */
+ if (strcmp(cachedTextureNode->fileName, fileName) == 0)
+ return cachedTextureNode;
+ cachedTextureNode = cachedTextureNode->next;
+ }
+
+ /* Try to load the texture */
+ char* fullPath = JoinPath(raytmxState->documentDirectory, fileName);
+ Texture2D texture = loadTextureOverride ? loadTextureOverride(fullPath) : LoadTexture(fullPath);
+ if (texture.id == 0) { /* If loading the texture failed */
+ TraceLog(LOG_ERROR, "RAYTMX: Unable to load texture \"%s\"", fullPath);
+ return NULL;
+ }
+
+ /* Create a new node in the list of known textures */
+ cachedTextureNode = (RaytmxCachedTextureNode*)MemAllocZero(sizeof(RaytmxCachedTextureNode));
+ cachedTextureNode->fileName = (char*)MemAllocZero((unsigned int)strlen(fileName) + 1);
+ StringCopy(cachedTextureNode->fileName, fileName);
+ cachedTextureNode->texture = texture;
+
+ /* Add to the cache */
+ if (raytmxState->texturesRoot == NULL)
+ raytmxState->texturesRoot = cachedTextureNode;
+ else {
+ RaytmxCachedTextureNode* cachedTextureIterator = raytmxState->texturesRoot;
+ while (cachedTextureIterator->next != NULL)
+ cachedTextureIterator = cachedTextureIterator->next;
+ cachedTextureIterator->next = cachedTextureNode;
+ }
+
+ return cachedTextureNode;
+}
+
+RaytmxCachedTemplateNode* LoadCachedTemplate(RaytmxState* raytmxState, const char* fileName) {
+ if (raytmxState == NULL || fileName == NULL)
+ return NULL;
+
+ /* First try to find an already-loaded template identified by the file name */
+ RaytmxCachedTemplateNode* cachedTemplateNode = raytmxState->templatesRoot;
+ while (cachedTemplateNode != NULL) {
+ /* If the file name associated with the node matches the given file name */
+ if (strcmp(cachedTemplateNode->fileName, fileName) == 0)
+ return cachedTemplateNode;
+ cachedTemplateNode = cachedTemplateNode->next;
+ }
+
+ /* Load the template from the external TX file */
+ char* fullPath = JoinPath(raytmxState->documentDirectory, fileName);
+ RaytmxObjectTemplate objectTemplate = LoadTX(fullPath);
+ if (!objectTemplate.isSuccess) { /* If loading the template failed */
+ TraceLog(LOG_ERROR, "RAYTMX: Unable to load template \"%s\"", fullPath);
+ return NULL;
+ }
+
+ /* Create a new node in the list of known templates */
+ cachedTemplateNode = (RaytmxCachedTemplateNode*)MemAllocZero(sizeof(RaytmxCachedTemplateNode));
+ cachedTemplateNode->fileName = (char*)MemAllocZero((unsigned int)strlen(fileName) + 1);
+ StringCopy(cachedTemplateNode->fileName, fileName);
+ cachedTemplateNode->objectTemplate = objectTemplate;
+
+ if (objectTemplate.hasTileset) { /* If the template contains a tileset in addition to an object */
+ /* In cases where the template's object references a tile (i.e. its 'gid' attribute is set), the template */
+ /* will have at most one tileset. Search the state object's list of tilesets and add this one if it's new. */
+ RaytmxTilesetNode* tilesetsIterator = raytmxState->tilesetsRoot;
+ bool isNew = true;
+ while (tilesetsIterator != NULL) {
+ /* If the existing tileset has a name, the template's tileset has a name, and they match OR if the */
+ /* existing tileset has a source, template's tileset has a source, and they match */
+ /* TODO: The comparison of sources without respect to directory should be reviewed as it could result in */
+ /* false negatives and duplicate tileset instances */
+ if ((tilesetsIterator->tileset.name != NULL && objectTemplate.tileset.name != NULL &&
+ strcmp(tilesetsIterator->tileset.name, objectTemplate.tileset.name) == 0) ||
+ (tilesetsIterator->tileset.source != NULL && objectTemplate.tileset.source != NULL &&
+ strcmp(tilesetsIterator->tileset.source, objectTemplate.tileset.source) == 0)) {
+ isNew = false;
+ break;
+ }
+ }
+ if (isNew) {
+ TmxTileset* tileset = AddTileset(raytmxState);
+ *tileset = objectTemplate.tileset;
+ }
+ }
+
+ /* Add to the cache */
+ if (raytmxState->templatesRoot == NULL)
+ raytmxState->templatesRoot = cachedTemplateNode;
+ else {
+ RaytmxCachedTemplateNode* cachedTemplateIterator = raytmxState->templatesRoot;
+ while (cachedTemplateIterator->next != NULL)
+ cachedTemplateIterator = cachedTemplateIterator->next;
+ cachedTemplateIterator->next = cachedTemplateNode;
+ }
+
+ return cachedTemplateNode;
+}
+
+Color GetColorFromHexString(const char* hex) {
+ Color color = BLACK; /* #define'd by raylib as { 0, 0, 0, 255 } */
+
+ size_t length = strlen(hex);
+ if (length < 6) /* If the hex string is too short to contain at least R, G, and B components */
+ return color;
+
+ /* Hex strings are in the form #AARRGGBB or #RRGGBB meaning alpha is optional. To avoid any special logic for the */
+ /* two cases where alpha is or isn't given, parsing will just be done backwards. Here, 'hex' is used as an */
+ /* iterator so it will begin at the end of the string. For example, for "#789abc" this begins at 'c'. */
+ hex += length - 1;
+
+ char component[] = "\0\0\0"; /* Used to hold the two-digit component (e.g. "55" or "ff") */
+ for (size_t i = 0; i < length / 2; i++) { /* Iterate three or four times with four meaning alpha was included */
+ component[1] = hex[0]; /* Store the char 'hex' points to. For "#789abc" this is 'c', 'a', or '8'. */
+ hex--; /* Point to the previous char in the string. For "#789abc" this now points to 'b', '9' or '7'. */
+ component[0] = hex[0]; /* Store the other char of the current component */
+ hex--; /* Iterate backwards again in preparation for the next component */
+ /* For "#789abc" the 'component' array is now "bc", "9a", or "78" */
+ switch (i) {
+ case 0: color.b = (unsigned char)strtoul(component, NULL, 16); break; /* e.g. "ff" -> 255 for blue */
+ case 1: color.g = (unsigned char)strtoul(component, NULL, 16); break; /* ...for green */
+ case 2: color.r = (unsigned char)strtoul(component, NULL, 16); break; /* ...for red */
+ case 3: color.a = (unsigned char)strtoul(component, NULL, 16); break; /* ...for alpha, maybe */
+ }
+ }
+
+ return color;
+}
+
+uint32_t GetGid(uint32_t rawGid, bool* isFlippedHorizontally, bool* isFlippedVertically, bool* isFlippedDiagonally,
+ bool* isRotatedHexagonal120) {
+ /* If the output parameters can be written to, output the status of the flip flags */
+ if (isFlippedHorizontally != NULL)
+ *isFlippedHorizontally = rawGid & FLIP_FLAG_HORIZONTAL;
+ if (isFlippedVertically != NULL)
+ *isFlippedVertically = rawGid & FLIP_FLAG_VERTICAL;
+ if (isFlippedDiagonally != NULL)
+ *isFlippedDiagonally = rawGid & FLIP_FLAG_DIAGONAL;
+ if (isRotatedHexagonal120 != NULL)
+ *isRotatedHexagonal120 = rawGid & FLIP_FLAG_ROTATE_120;
+
+ /* Return the integer with flip flags removed */
+ return rawGid & ~(FLIP_FLAG_HORIZONTAL | FLIP_FLAG_VERTICAL | FLIP_FLAG_DIAGONAL | FLIP_FLAG_ROTATE_120);
+}
+
+void* MemAllocZero(unsigned int size) {
+ void* buffer = MemAlloc(size); /* Reserve 'size' bytes of memory */
+ memset(buffer, 0, size); /* Initialize any values to zero, NULL, false, or an equivalent enum value */
+ return buffer;
+}
+
+/* "Get directory for a given filePath" */
+/* raylib's GetDirectoryPath() doesn't work as described so this is used in its place */
+char* GetDirectoryPath2(const char* filePath) {
+ static char directoryPath[260]; /* Max path length on Windows, the bottleneck, is 260 characters */
+ memset(directoryPath, '\0', 260);
+ size_t length = strlen(filePath);
+ /* Paths beginning with a Windows drive letter (C:\, D:\, etc.) or beginning with a slash are absolute paths */
+ if (length >= 2 && (filePath[1] == ':' || filePath[0] == '\\' || filePath[0] == '/')) { /* If absolute */
+ if (IsPathFile(filePath)) /* If filePath points to a file, and we already know it's absolute */
+ StringCopy(directoryPath, filePath);
+ else { /* If filePath points to a directory, and we already know it's absolute */
+ StringCopy(directoryPath, filePath);
+ return directoryPath;
+ }
+ } else /* If filePath is relative */
+ StringCopy(directoryPath, JoinPath(GetWorkingDirectory(), filePath));
+
+ /* The goal is to return part of filePath, up to the last slash */
+ length = strlen(directoryPath);
+ char* iterator = directoryPath + length - 1;
+ /* Iterate backwards until a slash is found */
+ while (iterator != directoryPath && *iterator != '\0' && *iterator != '\\' && *iterator != '/')
+ iterator -= 1;
+ *(iterator + 1) = '\0'; /* Place a null terminator after the slash to effectively end the string there */
+ return directoryPath;
+}
+
+char* JoinPath(const char* prefix, const char* suffix) {
+ static char joinedPath[260]; /* Max path length on Windows, the bottleneck, is 260 characters */
+ memset(joinedPath, '\0', 260);
+ StringCopy(joinedPath, prefix);
+ size_t prefixLength = strlen(prefix);
+ if ((prefixLength >= 1) && (joinedPath[prefixLength - 1] != '/') && (joinedPath[prefixLength - 1] != '\\'))
+#ifdef _WIN32
+ joinedPath[prefixLength] = '\\'; /* Append the path with a '\\' separator */
+#else
+ joinedPath[prefixLength] = '/'; /* Append the path with a '/' separator */
+#endif
+ const char* suffixStart = suffix;
+ size_t suffixLength = strlen(suffix);
+ if (suffixLength >= 2 && suffix[0] == '.' && (suffix[1] == '/' || suffix[1] == '\\'))
+ suffixStart += 2; /* Skip over the "this directory" part (e.g. "./a.tsx" -> "a.tsx") */
+ /* Note: ".." is kept in the joined path intentionally although it is possible to exceed 260 characters. TODO? */
+ StringConcatenate(joinedPath, suffixStart);
+ return joinedPath;
+}
+
+void StringCopy(char* destination, const char* source) {
+#if (!defined _MSC_VER || defined _CRT_SECURE_NO_WARNINGS)
+ /* This is for build environments where "[M]icro[S]oft [C]ompiler [VER]sion" is not defined, meaning the compiler */
+ /* is one other than MSVC, or where MSVC is being used but the security deprecation warning has been disabled. */
+ /* MSVC does not consider strcpy() to be secure because it's somewhat unbounded. In practice, it's fine. */
+ strcpy(destination, source);
+#else
+ /* This keeps MSVC happy by providing the supposed size of the destination buffer. However, these buffers are */
+ /* dynamically allocated so there's no quick way to get the actual size. The best solution is to use strlen() */
+ /* but this makes the function O(2n) vs. the alternative's O(n). */
+ strcpy_s(destination, strlen(source) + 1, source); /* + 1 for the terminator */
+#endif
+}
+
+void StringCopyN(char* destination, const char* source, size_t number) {
+#if (!defined _MSC_VER || defined _CRT_SECURE_NO_WARNINGS)
+ strncpy(destination, source, number);
+#else
+ strncpy_s(destination, number + 1, source, number);
+#endif
+}
+
+void StringConcatenate(char* destination, const char* source) {
+#if (!defined _MSC_VER || defined _CRT_SECURE_NO_WARNINGS)
+ strcat(destination, source);
+#else
+ strcat_s(destination, strlen(destination) + strlen(source) + 1, source);
+#endif
+}
+
+#endif /* RAYTMX_IMPLEMENTATION */
+
+#endif /* RAYTMX_H */