/* Copyright (c) 2024 Alex Diener This software is provided 'as-is', without any express or implied warranty. In no event will the authors be held liable for any damages arising from the use of this software. Permission is granted to anyone to use this software for any purpose, including commercial applications, and to alter it and redistribute it freely, subject to the following restrictions: 1. The origin of this software must not be misrepresented; you must not claim that you wrote the original software. If you use this software in a product, an acknowledgment in the product documentation would be appreciated but is not required. 2. Altered source versions must be plainly marked as such, and must not be misrepresented as being the original software. 3. This notice may not be removed or altered from any source distribution. Alex Diener alex@ludobloom.com */ #include "tileset/TileAdjacencyBehavior.h" #include "utilities/IOUtilities.h" #include "stem_core.h" #include #include TileAdjacencyBehavior TileAdjacencyBehavior_init(TileID ownTileID, bool exclusive, TileAdjacencyRule rule, Rect4i sliceInsets, unsigned int overlayCount, TileAdjacencyOverlay * overlays, bool copy) { TileAdjacencyBehavior behavior; behavior.ownTileID = ownTileID; behavior.exclusive = exclusive; if (copy) { behavior.rule = TileAdjacencyRule_copy(&rule); } else { behavior.rule = rule; } behavior.sliceInsets = sliceInsets; behavior.overlayCount = overlayCount; if (copy) { behavior.overlays = memdup(overlays, overlayCount * sizeof(*behavior.overlays)); } else { behavior.overlays = overlays; } behavior.totalBounds = RECT4i_EMPTY; return behavior; } TileAdjacencyBehavior TileAdjacencyBehavior_copy(TileAdjacencyBehavior * behavior) { TileAdjacencyBehavior copy = TileAdjacencyBehavior_init(behavior->ownTileID, behavior->exclusive, behavior->rule, behavior->sliceInsets, behavior->overlayCount, behavior->overlays, true); copy.totalBounds = behavior->totalBounds; return copy; } void TileAdjacencyBehavior_dispose(TileAdjacencyBehavior * behavior) { TileAdjacencyRule_dispose(&behavior->rule); free(behavior->overlays); } #define RULE_TYPE_ENUM_VALUES { \ {"and", TILE_CONDITION_AND}, \ {"or", TILE_CONDITION_OR}, \ {"match_single", TILE_CONDITION_MATCH_SINGLE}, \ {"nonmatch_single", TILE_CONDITION_NONMATCH_SINGLE}, \ {"match_multiple", TILE_CONDITION_MATCH_MULTIPLE}, \ {"nonmatch_multiple", TILE_CONDITION_NONMATCH_MULTIPLE} \ } static void writeRule(SerializationContext * context, TileAdjacencyRule * rule) { Serialization_enumKeyValue ruleTypeEnumValues[] = RULE_TYPE_ENUM_VALUES; call_virtual(writeEnumeration8, context, "type", rule->type, sizeof_count(ruleTypeEnumValues), ruleTypeEnumValues); switch (rule->type) { case TILE_CONDITION_AND: case TILE_CONDITION_OR: call_virtual(beginArray, context, "conditions"); for (unsigned int conditionIndex = 0; conditionIndex < rule->rule.conditionList.count; conditionIndex++) { call_virtual(beginStructure, context, NULL); writeRule(context, &rule->rule.conditionList.conditions[conditionIndex]); call_virtual(endStructure, context); } call_virtual(endArray, context); break; case TILE_CONDITION_MATCH_SINGLE: case TILE_CONDITION_NONMATCH_SINGLE: call_virtual(writeInt16, context, "offset_x", rule->rule.single.offset.x); call_virtual(writeInt16, context, "offset_y", rule->rule.single.offset.y); call_virtual(writeUInt32, context, "tile_id", rule->rule.single.tileID); break; case TILE_CONDITION_MATCH_MULTIPLE: case TILE_CONDITION_NONMATCH_MULTIPLE: call_virtual(writeInt16, context, "offset_x", rule->rule.multiple.offset.x); call_virtual(writeInt16, context, "offset_y", rule->rule.multiple.offset.y); call_virtual(beginArray, context, "tile_ids"); for (unsigned int tileIndex = 0; tileIndex < rule->rule.multiple.tileIDCount; tileIndex++) { call_virtual(writeUInt32, context, NULL, rule->rule.multiple.tileIDs[tileIndex]); } call_virtual(endArray, context); break; } } void TileAdjacencyBehavior_serializeMinimal(TileAdjacencyBehavior * behavior, compat_type(SerializationContext *) serializationContext) { SerializationContext * context = serializationContext; call_virtual(writeUInt32, context, "tile_id", behavior->ownTileID); call_virtual(writeBoolean, context, "exclusive", behavior->exclusive); call_virtual(beginStructure, context, "rule"); writeRule(context, &behavior->rule); call_virtual(endStructure, context); call_virtual(beginStructure, context, "slice_insets"); call_virtual(writeUInt16, context, "x_min", behavior->sliceInsets.xMin); call_virtual(writeUInt16, context, "x_max", behavior->sliceInsets.xMax); call_virtual(writeUInt16, context, "y_min", behavior->sliceInsets.yMin); call_virtual(writeUInt16, context, "y_max", behavior->sliceInsets.yMax); call_virtual(endStructure, context); call_virtual(beginArray, context, "overlays"); for (unsigned int overlayIndex = 0; overlayIndex < behavior->overlayCount; overlayIndex++) { call_virtual(beginStructure, context, NULL); call_virtual(writeUInt32, context, "image_id", behavior->overlays[overlayIndex].imageID); call_virtual(writeInt16, context, "offset_x", behavior->overlays[overlayIndex].offset.x); call_virtual(writeInt16, context, "offset_y", behavior->overlays[overlayIndex].offset.y); call_virtual(beginStructure, context, "slice_insets"); call_virtual(writeUInt16, context, "x_min", behavior->overlays[overlayIndex].sliceInsets.xMin); call_virtual(writeUInt16, context, "x_max", behavior->overlays[overlayIndex].sliceInsets.xMax); call_virtual(writeUInt16, context, "y_min", behavior->overlays[overlayIndex].sliceInsets.yMin); call_virtual(writeUInt16, context, "y_max", behavior->overlays[overlayIndex].sliceInsets.yMax); call_virtual(endStructure, context); call_virtual(writeInt32, context, "draw_priority", behavior->overlays[overlayIndex].drawPriority); call_virtual(endStructure, context); } call_virtual(endArray, context); } static TileAdjacencyRule readRule(DeserializationContext * context) { TileAdjacencyRule rule; Serialization_enumKeyValue ruleTypeEnumValues[] = RULE_TYPE_ENUM_VALUES; rule.type = call_virtual(readEnumeration8, context, "type", sizeof_count(ruleTypeEnumValues), ruleTypeEnumValues); switch (rule.type) { case TILE_CONDITION_AND: case TILE_CONDITION_OR: { rule.rule.conditionList.count = 0; unsigned int conditionCount = call_virtual(beginArray, context, "conditions"); if (conditionCount == 0 || conditionCount > TILE_ADJACENCY_CONDITION_COUNT_MAX) { context->status = SERIALIZATION_ERROR_ARRAY_COUNT_OUT_OF_RANGE; return (TileAdjacencyRule) {.type = TILE_CONDITION_MATCH_SINGLE}; } rule.rule.conditionList.conditions = malloc(conditionCount * sizeof(*rule.rule.conditionList.conditions)); for (unsigned int conditionIndex = 0; conditionIndex < conditionCount; conditionIndex++) { call_virtual(beginStructure, context, NULL); rule.rule.conditionList.conditions[conditionIndex] = readRule(context); call_virtual(endStructure, context); if (context->status != SERIALIZATION_ERROR_OK) { for (unsigned int conditionIndex2 = 0; conditionIndex2 < conditionIndex; conditionIndex2++) { TileAdjacencyRule_dispose(&rule.rule.conditionList.conditions[conditionIndex2]); } free(rule.rule.conditionList.conditions); return (TileAdjacencyRule) {.type = TILE_CONDITION_MATCH_SINGLE}; } rule.rule.conditionList.count++; } call_virtual(endArray, context); break; } case TILE_CONDITION_MATCH_SINGLE: case TILE_CONDITION_NONMATCH_SINGLE: rule.rule.single.offset.x = call_virtual(readInt16, context, "offset_x"); rule.rule.single.offset.y = call_virtual(readInt16, context, "offset_y"); rule.rule.single.tileID = call_virtual(readUInt32, context, "tile_id"); break; case TILE_CONDITION_MATCH_MULTIPLE: case TILE_CONDITION_NONMATCH_MULTIPLE: rule.rule.multiple.offset.x = call_virtual(readInt16, context, "offset_x"); rule.rule.multiple.offset.y = call_virtual(readInt16, context, "offset_y"); rule.rule.multiple.tileIDCount = call_virtual(beginArray, context, "tile_ids"); if (rule.rule.multiple.tileIDCount == 0 || rule.rule.multiple.tileIDCount > TILE_ADJACENCY_MATCH_COUNT_MAX) { context->status = SERIALIZATION_ERROR_ARRAY_COUNT_OUT_OF_RANGE; return (TileAdjacencyRule) {.type = TILE_CONDITION_MATCH_SINGLE}; } rule.rule.multiple.tileIDs = malloc(rule.rule.multiple.tileIDCount * sizeof(*rule.rule.multiple.tileIDs)); for (unsigned int tileIndex = 0; tileIndex < rule.rule.multiple.tileIDCount; tileIndex++) { rule.rule.multiple.tileIDs[tileIndex] = call_virtual(readUInt32, context, NULL); } call_virtual(endArray, context); break; } return rule; } bool TileAdjacencyBehavior_deserializeMinimal(TileAdjacencyBehavior * behavior, compat_type(DeserializationContext *) deserializationContext, uint16_t formatVersion) { DeserializationContext * context = deserializationContext; if (context->status != SERIALIZATION_ERROR_OK || formatVersion > TILE_ADJACENCY_FORMAT_VERSION) { return NULL; } behavior->ownTileID = call_virtual(readUInt32, context, "tile_id"); behavior->exclusive = call_virtual(readBoolean, context, "exclusive"); call_virtual(beginStructure, context, "rule"); behavior->rule = readRule(context); call_virtual(endStructure, context); call_virtual(beginStructure, context, "slice_insets"); behavior->sliceInsets.xMin = call_virtual(readUInt16, context, "x_min"); behavior->sliceInsets.xMax = call_virtual(readUInt16, context, "x_max"); behavior->sliceInsets.yMin = call_virtual(readUInt16, context, "y_min"); behavior->sliceInsets.yMax = call_virtual(readUInt16, context, "y_max"); call_virtual(endStructure, context); behavior->overlayCount = call_virtual(beginArray, context, "overlays"); behavior->overlays = NULL; if (behavior->overlayCount > TILE_ADJACENCY_OVERLAY_COUNT_MAX) { context->status = SERIALIZATION_ERROR_ARRAY_COUNT_OUT_OF_RANGE; TileAdjacencyBehavior_dispose(behavior); return false; } if (behavior->overlayCount > 0) { behavior->overlays = malloc(behavior->overlayCount * sizeof(*behavior->overlays)); for (unsigned int overlayIndex = 0; overlayIndex < behavior->overlayCount; overlayIndex++) { call_virtual(beginStructure, context, NULL); behavior->overlays[overlayIndex].imageID = call_virtual(readUInt32, context, "image_id"); behavior->overlays[overlayIndex].offset.x = call_virtual(readInt16, context, "offset_x"); behavior->overlays[overlayIndex].offset.y = call_virtual(readInt16, context, "offset_y"); call_virtual(beginStructure, context, "slice_insets"); behavior->overlays[overlayIndex].sliceInsets.xMin = call_virtual(readUInt16, context, "x_min"); behavior->overlays[overlayIndex].sliceInsets.xMax = call_virtual(readUInt16, context, "x_max"); behavior->overlays[overlayIndex].sliceInsets.yMin = call_virtual(readUInt16, context, "y_min"); behavior->overlays[overlayIndex].sliceInsets.yMax = call_virtual(readUInt16, context, "y_max"); call_virtual(endStructure, context); behavior->overlays[overlayIndex].drawPriority = call_virtual(readInt32, context, "draw_priority"); behavior->overlays[overlayIndex].imageIndex = UINT_MAX; call_virtual(endStructure, context); } } call_virtual(endArray, context); behavior->totalBounds = RECT4i_EMPTY; if (context->status != SERIALIZATION_ERROR_OK) { TileAdjacencyBehavior_dispose(behavior); return false; } return true; } void TileAdjacencyBehavior_addOverlay(TileAdjacencyBehavior * behavior, TileAdjacencyOverlay overlay) { TileAdjacencyBehavior_insertOverlay(behavior, behavior->overlayCount, overlay); } void TileAdjacencyBehavior_insertOverlay(TileAdjacencyBehavior * behavior, unsigned int overlayIndex, TileAdjacencyOverlay overlay) { assert(overlayIndex <= behavior->overlayCount); behavior->overlays = realloc(behavior->overlays, (behavior->overlayCount + 1) * sizeof(*behavior->overlays)); for (unsigned int overlayIndex2 = behavior->overlayCount; overlayIndex2 > overlayIndex; overlayIndex2--) { behavior->overlays[overlayIndex2] = behavior->overlays[overlayIndex2 - 1]; } behavior->overlays[overlayIndex] = overlay; behavior->overlayCount++; } void TileAdjacencyBehavior_removeOverlayAtIndex(TileAdjacencyBehavior * behavior, unsigned int overlayIndex) { if (overlayIndex < behavior->overlayCount) { behavior->overlayCount--; for (; overlayIndex < behavior->overlayCount; overlayIndex++) { behavior->overlays[overlayIndex] = behavior->overlays[overlayIndex + 1]; } } } void TileAdjacencyBehavior_reorderOverlay(TileAdjacencyBehavior * behavior, unsigned int fromOverlayIndex, unsigned int toOverlayIndex) { assert(fromOverlayIndex < behavior->overlayCount); assert(toOverlayIndex < behavior->overlayCount); TileAdjacencyOverlay swap = behavior->overlays[fromOverlayIndex]; int direction = (toOverlayIndex > fromOverlayIndex) * 2 - 1; for (unsigned int swapIndex = fromOverlayIndex; swapIndex != toOverlayIndex; swapIndex += direction) { behavior->overlays[swapIndex] = behavior->overlays[swapIndex + direction]; } behavior->overlays[toOverlayIndex] = swap; } void TileAdjacencyBehavior_calculateTotalBounds(TileAdjacencyBehavior * behavior, ImageCollection * imageCollection) { behavior->totalBounds = RECT4i_EMPTY; for (unsigned int overlayIndex = 0; overlayIndex < behavior->overlayCount; overlayIndex++) { if (behavior->overlays[overlayIndex].imageIndex < imageCollection->imageCount) { Rect4i overlayBounds = Rect4i_fromPositionAndSize(behavior->overlays[overlayIndex].offset, VECTOR2i(imageCollection->images[behavior->overlays[overlayIndex].imageIndex].size.x, imageCollection->images[behavior->overlays[overlayIndex].imageIndex].size.y)); overlayBounds = Rect4i_insetMargins(overlayBounds, behavior->overlays[overlayIndex].sliceInsets); behavior->totalBounds = Rect4i_union(behavior->totalBounds, overlayBounds); } } } static bool isTileMatch(TileID tileID, Vector2i offset, Vector2i position, TileID ownTileID, TileQueryCallback queryCallback, void * callbackContext) { if (tileID == TILE_ID_SELF) { tileID = ownTileID; } return queryCallback(Vector2i_add(position, offset), callbackContext) == tileID; } static bool isAnyTileMatch(unsigned int tileIDCount, TileID * tileIDs, Vector2i offset, Vector2i position, TileID ownTileID, TileQueryCallback queryCallback, void * callbackContext) { for (unsigned int tileIndex = 0; tileIndex < tileIDCount; tileIndex++) { if (isTileMatch(tileIDs[tileIndex], offset, position, ownTileID, queryCallback, callbackContext)) { return true; } } return false; } static bool isRuleApplicable(TileAdjacencyRule * rule, Vector2i position, TileID ownTileID, TileQueryCallback queryCallback, void * callbackContext) { switch (rule->type) { case TILE_CONDITION_AND: for (unsigned int conditionIndex = 0; conditionIndex < rule->rule.conditionList.count; conditionIndex++) { if (!isRuleApplicable(&rule->rule.conditionList.conditions[conditionIndex], position, ownTileID, queryCallback, callbackContext)) { return false; } } return true; case TILE_CONDITION_OR: for (unsigned int conditionIndex = 0; conditionIndex < rule->rule.conditionList.count; conditionIndex++) { if (isRuleApplicable(&rule->rule.conditionList.conditions[conditionIndex], position, ownTileID, queryCallback, callbackContext)) { return true; } } return false; case TILE_CONDITION_MATCH_SINGLE: return isTileMatch(rule->rule.single.tileID, rule->rule.single.offset, position, ownTileID, queryCallback, callbackContext); case TILE_CONDITION_NONMATCH_SINGLE: return !isTileMatch(rule->rule.single.tileID, rule->rule.single.offset, position, ownTileID, queryCallback, callbackContext); case TILE_CONDITION_MATCH_MULTIPLE: return isAnyTileMatch(rule->rule.multiple.tileIDCount, rule->rule.multiple.tileIDs, rule->rule.multiple.offset, position, ownTileID, queryCallback, callbackContext); case TILE_CONDITION_NONMATCH_MULTIPLE: return !isAnyTileMatch(rule->rule.multiple.tileIDCount, rule->rule.multiple.tileIDs, rule->rule.multiple.offset, position, ownTileID, queryCallback, callbackContext); } return false; } bool TileAdjacencyBehavior_isApplicable(TileAdjacencyBehavior * behavior, Vector2i position, TileQueryCallback queryCallback, void * callbackContext) { return isRuleApplicable(&behavior->rule, position, behavior->ownTileID, queryCallback, callbackContext); } TileAdjacencyRule TileAdjacencyRule_copy(TileAdjacencyRule * rule) { TileAdjacencyRule copy = *rule; switch (copy.type) { case TILE_CONDITION_AND: case TILE_CONDITION_OR: copy.rule.conditionList.conditions = malloc(copy.rule.conditionList.count * sizeof(*copy.rule.conditionList.conditions)); for (unsigned int conditionIndex = 0; conditionIndex < copy.rule.conditionList.count; conditionIndex++) { copy.rule.conditionList.conditions[conditionIndex] = TileAdjacencyRule_copy(&rule->rule.conditionList.conditions[conditionIndex]); } break; case TILE_CONDITION_MATCH_SINGLE: case TILE_CONDITION_NONMATCH_SINGLE: break; case TILE_CONDITION_MATCH_MULTIPLE: case TILE_CONDITION_NONMATCH_MULTIPLE: copy.rule.multiple.tileIDs = memdup(copy.rule.multiple.tileIDs, copy.rule.multiple.tileIDCount * sizeof(*copy.rule.multiple.tileIDs)); break; } return copy; } void TileAdjacencyRule_dispose(TileAdjacencyRule * rule) { switch (rule->type) { case TILE_CONDITION_AND: case TILE_CONDITION_OR: for (unsigned int conditionIndex = 0; conditionIndex < rule->rule.conditionList.count; conditionIndex++) { TileAdjacencyRule_dispose(&rule->rule.conditionList.conditions[conditionIndex]); } free(rule->rule.conditionList.conditions); break; case TILE_CONDITION_MATCH_SINGLE: case TILE_CONDITION_NONMATCH_SINGLE: break; case TILE_CONDITION_MATCH_MULTIPLE: case TILE_CONDITION_NONMATCH_MULTIPLE: free(rule->rule.multiple.tileIDs); break; } } bool TileAdjacencyRule_isEqual(TileAdjacencyRule * rule1, TileAdjacencyRule * rule2) { if (rule1->type != rule2->type) { return false; } switch (rule1->type) { case TILE_CONDITION_AND: case TILE_CONDITION_OR: if (rule1->rule.conditionList.count != rule2->rule.conditionList.count) { return false; } for (unsigned int conditionIndex = 0; conditionIndex < rule1->rule.conditionList.count; conditionIndex++) { if (!TileAdjacencyRule_isEqual(&rule1->rule.conditionList.conditions[conditionIndex], &rule2->rule.conditionList.conditions[conditionIndex])) { return false; } } break; case TILE_CONDITION_MATCH_SINGLE: case TILE_CONDITION_NONMATCH_SINGLE: if (rule1->rule.single.offset.x != rule2->rule.single.offset.x || rule1->rule.single.offset.y != rule2->rule.single.offset.y || rule1->rule.single.tileID != rule2->rule.single.tileID) { return false; } break; case TILE_CONDITION_MATCH_MULTIPLE: case TILE_CONDITION_NONMATCH_MULTIPLE: if (rule1->rule.multiple.offset.x != rule2->rule.multiple.offset.x || rule1->rule.multiple.offset.y != rule2->rule.multiple.offset.y || rule1->rule.multiple.tileIDCount != rule2->rule.multiple.tileIDCount) { return false; } for (unsigned int tileIDIndex = 0; tileIDIndex < rule1->rule.multiple.tileIDCount; tileIDIndex++) { if (rule1->rule.multiple.tileIDs[tileIDIndex] != rule2->rule.multiple.tileIDs[tileIDIndex]) { return false; } } break; } return true; }