BetterTerrain.gd 36 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159
  1. @tool
  2. extends Node
  3. ## A [TileMapLayer] terrain / auto-tiling system.
  4. ##
  5. ## This is a drop-in replacement for Godot 4's tilemap terrain system, offering
  6. ## more versatile and straightforward autotiling. It can be used with any
  7. ## existing [TileMapLayer] or [TileSet], either through the editor plugin, or
  8. ## directly via code.
  9. ## [br][br]
  10. ## The [b]BetterTerrain[/b] class contains only static functions, each of which
  11. ## either takes a [TileMapLayer], a [TileSet], and sometimes a [TileData].
  12. ## Meta-data is embedded inside the [TileSet] and the [TileData] types to store
  13. ## the terrain information. See [method Object.get_meta] for information.
  14. ## [br][br]
  15. ## Once terrain is set up, it can be written to the tilemap using [method set_cells].
  16. ## Similar to Godot 3.x, setting the cells does not run the terrain solver, so once
  17. ## the cells have been set, you need to call an update function such as [method update_terrain_cells].
  18. ## The meta-data key used to store terrain information.
  19. const TERRAIN_META = &"_better_terrain"
  20. ## The current version. Used to handle future upgrades.
  21. const TERRAIN_SYSTEM_VERSION = "0.2"
  22. var _tile_cache = {}
  23. var rng = RandomNumberGenerator.new()
  24. var use_seed := true
  25. ## A helper class that provides functions detailing valid peering bits and
  26. ## polygons for different tile types.
  27. var data := load("res://addons/better-terrain/BetterTerrainData.gd"):
  28. get:
  29. return data
  30. enum TerrainType {
  31. MATCH_TILES, ## Selects tiles by matching against adjacent tiles.
  32. MATCH_VERTICES, ## Select tiles by analysing vertices, similar to wang-style tiles.
  33. CATEGORY, ## Declares a matching type for more sophisticated rules.
  34. DECORATION, ## Fills empty tiles by matching adjacent tiles
  35. MAX,
  36. }
  37. enum TileCategory {
  38. EMPTY = -1, ## An empty cell, or a tile marked as decoration
  39. NON_TERRAIN = -2, ## A non-empty cell that does not contain a terrain tile
  40. ERROR = -3
  41. }
  42. enum SymmetryType {
  43. NONE,
  44. MIRROR, ## Horizontally mirror
  45. FLIP, ## Vertically flip
  46. REFLECT, ## All four reflections
  47. ROTATE_CLOCKWISE,
  48. ROTATE_COUNTER_CLOCKWISE,
  49. ROTATE_180,
  50. ROTATE_ALL, ## All four rotated forms
  51. ALL ## All rotated and reflected forms
  52. }
  53. func _intersect(first: Array, second: Array) -> bool:
  54. if first.size() > second.size():
  55. return _intersect(second, first) # Array 'has' is fast compared to gdscript loop
  56. for f in first:
  57. if second.has(f):
  58. return true
  59. return false
  60. # Meta-data functions
  61. func _get_terrain_meta(ts: TileSet) -> Dictionary:
  62. return ts.get_meta(TERRAIN_META) if ts and ts.has_meta(TERRAIN_META) else {
  63. terrains = [],
  64. decoration = ["Decoration", Color.DIM_GRAY, TerrainType.DECORATION, [], {path = "res://addons/better-terrain/icons/Decoration.svg"}],
  65. version = TERRAIN_SYSTEM_VERSION
  66. }
  67. func _set_terrain_meta(ts: TileSet, meta : Dictionary) -> void:
  68. ts.set_meta(TERRAIN_META, meta)
  69. ts.emit_changed()
  70. func _get_tile_meta(td: TileData) -> Dictionary:
  71. return td.get_meta(TERRAIN_META) if td.has_meta(TERRAIN_META) else {
  72. type = TileCategory.NON_TERRAIN
  73. }
  74. func _set_tile_meta(ts: TileSet, td: TileData, meta) -> void:
  75. td.set_meta(TERRAIN_META, meta)
  76. ts.emit_changed()
  77. func _get_cache(ts: TileSet) -> Array:
  78. if _tile_cache.has(ts):
  79. return _tile_cache[ts]
  80. var cache := []
  81. if !ts:
  82. return cache
  83. _tile_cache[ts] = cache
  84. var watcher = Node.new()
  85. watcher.set_script(load("res://addons/better-terrain/Watcher.gd"))
  86. watcher.tileset = ts
  87. watcher.trigger.connect(_purge_cache.bind(ts))
  88. add_child(watcher)
  89. ts.changed.connect(watcher.activate)
  90. var types = {}
  91. var ts_meta := _get_terrain_meta(ts)
  92. for t in ts_meta.terrains.size():
  93. var terrain = ts_meta.terrains[t]
  94. var bits = terrain[3].duplicate()
  95. bits.push_back(t)
  96. types[t] = bits
  97. cache.push_back([])
  98. # Decoration
  99. types[-1] = [TileCategory.EMPTY]
  100. cache.push_back([[-1, Vector2.ZERO, -1, {}, 1.0]])
  101. for s in ts.get_source_count():
  102. var source_id := ts.get_source_id(s)
  103. var source := ts.get_source(source_id) as TileSetAtlasSource
  104. if !source:
  105. continue
  106. source.changed.connect(watcher.activate)
  107. for c in source.get_tiles_count():
  108. var coord := source.get_tile_id(c)
  109. for a in source.get_alternative_tiles_count(coord):
  110. var alternate := source.get_alternative_tile_id(coord, a)
  111. var td := source.get_tile_data(coord, alternate)
  112. var td_meta := _get_tile_meta(td)
  113. if td_meta.type < TileCategory.EMPTY or td_meta.type >= cache.size():
  114. continue
  115. td.changed.connect(watcher.activate)
  116. var peering := {}
  117. for key in td_meta.keys():
  118. if !(key is int):
  119. continue
  120. var targets := []
  121. for k in types:
  122. if _intersect(types[k], td_meta[key]):
  123. targets.push_back(k)
  124. peering[key] = targets
  125. # Decoration tiles without peering are skipped
  126. if td_meta.type == TileCategory.EMPTY and !peering:
  127. continue
  128. var symmetry = td_meta.get("symmetry", SymmetryType.NONE)
  129. # Branch out no symmetry tiles early
  130. if symmetry == SymmetryType.NONE:
  131. cache[td_meta.type].push_back([source_id, coord, alternate, peering, td.probability])
  132. continue
  133. # calculate the symmetry order for this tile
  134. var symmetry_order := 0
  135. for flags in data.symmetry_mapping[symmetry]:
  136. var symmetric_peering = data.peering_bits_after_symmetry(peering, flags)
  137. if symmetric_peering == peering:
  138. symmetry_order += 1
  139. var adjusted_probability = td.probability / symmetry_order
  140. for flags in data.symmetry_mapping[symmetry]:
  141. var symmetric_peering = data.peering_bits_after_symmetry(peering, flags)
  142. cache[td_meta.type].push_back([source_id, coord, alternate | flags, symmetric_peering, adjusted_probability])
  143. return cache
  144. func _get_cache_terrain(ts_meta : Dictionary, index: int) -> Array:
  145. # the cache and the terrains in ts_meta don't line up because
  146. # decorations are cached too
  147. if index < 0 or index >= ts_meta.terrains.size():
  148. return ts_meta.decoration
  149. return ts_meta.terrains[index]
  150. func _purge_cache(ts: TileSet) -> void:
  151. _tile_cache.erase(ts)
  152. for c in get_children():
  153. if c.tileset == ts:
  154. c.tidy()
  155. break
  156. func _clear_invalid_peering_types(ts: TileSet) -> void:
  157. var ts_meta := _get_terrain_meta(ts)
  158. var cache := _get_cache(ts)
  159. for t in cache.size():
  160. var type = _get_cache_terrain(ts_meta, t)[2]
  161. var valid_peering_types = data.get_terrain_peering_cells(ts, type)
  162. for c in cache[t]:
  163. if c[0] < 0:
  164. continue
  165. var source := ts.get_source(c[0]) as TileSetAtlasSource
  166. if !source:
  167. continue
  168. var td := source.get_tile_data(c[1], c[2])
  169. var td_meta := _get_tile_meta(td)
  170. for peering in c[3].keys():
  171. if valid_peering_types.has(peering):
  172. continue
  173. td_meta.erase(peering)
  174. _set_tile_meta(ts, td, td_meta)
  175. # Not strictly necessary
  176. _purge_cache(ts)
  177. func _has_invalid_peering_types(ts: TileSet) -> bool:
  178. var ts_meta := _get_terrain_meta(ts)
  179. var cache := _get_cache(ts)
  180. for t in cache.size():
  181. var type = _get_cache_terrain(ts_meta, t)[2]
  182. var valid_peering_types = data.get_terrain_peering_cells(ts, type)
  183. for c in cache[t]:
  184. for peering in c[3].keys():
  185. if !valid_peering_types.has(peering):
  186. return true
  187. return false
  188. func _update_terrain_data(ts: TileSet) -> void:
  189. var ts_meta = _get_terrain_meta(ts)
  190. var previous_version = ts_meta.get("version")
  191. # First release: no version info
  192. if !ts_meta.has("version"):
  193. ts_meta["version"] = "0.0"
  194. # 0.0 -> 0.1: add categories
  195. if ts_meta.version == "0.0":
  196. for t in ts_meta.terrains:
  197. if t.size() == 3:
  198. t.push_back([])
  199. ts_meta.version = "0.1"
  200. # 0.1 -> 0.2: add decoration tiles and terrain icons
  201. if ts_meta.version == "0.1":
  202. # Add terrain icon containers
  203. for t in ts_meta.terrains:
  204. if t.size() == 4:
  205. t.push_back({})
  206. # Add default decoration data
  207. ts_meta["decoration"] = ["Decoration", Color.DIM_GRAY, TerrainType.DECORATION, [], {path = "res://addons/better-terrain/icons/Decoration.svg"}]
  208. ts_meta.version = "0.2"
  209. if previous_version != ts_meta.version:
  210. _set_terrain_meta(ts, ts_meta)
  211. func _weighted_selection(choices: Array, apply_empty_probability: bool):
  212. if choices.is_empty():
  213. return null
  214. var weight = choices.reduce(func(a, c): return a + c[4], 0.0)
  215. if apply_empty_probability and weight < 1.0 and rng.randf() > weight:
  216. return [-1, Vector2.ZERO, -1, null, 1.0]
  217. if choices.size() == 1:
  218. return choices[0]
  219. if weight == 0.0:
  220. return choices[rng.randi() % choices.size()]
  221. var pick = rng.randf() * weight
  222. for c in choices:
  223. if pick < c[4]:
  224. return c
  225. pick -= c[4]
  226. return choices.back()
  227. func _weighted_selection_seeded(choices: Array, coord: Vector2i, apply_empty_probability: bool):
  228. if use_seed:
  229. rng.seed = hash(coord)
  230. return _weighted_selection(choices, apply_empty_probability)
  231. func _update_tile_tiles(tm: TileMapLayer, coord: Vector2i, types: Dictionary, cache: Array, apply_empty_probability: bool):
  232. var type = types[coord]
  233. const reward := 3
  234. var penalty := -2000 if apply_empty_probability else -10
  235. var best_score := -1000 # Impossibly bad score
  236. var best := []
  237. for t in cache[type]:
  238. var score := 0
  239. for peering in t[3]:
  240. score += reward if t[3][peering].has(types[tm.get_neighbor_cell(coord, peering)]) else penalty
  241. if score > best_score:
  242. best_score = score
  243. best = [t]
  244. elif score == best_score:
  245. best.append(t)
  246. return _weighted_selection_seeded(best, coord, apply_empty_probability)
  247. func _probe(tm: TileMapLayer, coord: Vector2i, peering: int, type: int, types: Dictionary) -> int:
  248. var targets = data.associated_vertex_cells(tm, coord, peering)
  249. targets = targets.map(func(c): return types[c])
  250. var first = targets[0]
  251. if targets.all(func(t): return t == first):
  252. return first
  253. # if different, use the lowest non-same
  254. targets = targets.filter(func(t): return t != type)
  255. return targets.reduce(func(a, t): return min(a, t))
  256. func _update_tile_vertices(tm: TileMapLayer, coord: Vector2i, types: Dictionary, cache: Array):
  257. var type = types[coord]
  258. const reward := 3
  259. const penalty := -10
  260. var best_score := -1000 # Impossibly bad score
  261. var best := []
  262. for t in cache[type]:
  263. var score := 0
  264. for peering in t[3]:
  265. score += reward if _probe(tm, coord, peering, type, types) in t[3][peering] else penalty
  266. if score > best_score:
  267. best_score = score
  268. best = [t]
  269. elif score == best_score:
  270. best.append(t)
  271. return _weighted_selection_seeded(best, coord, false)
  272. func _update_tile_immediate(tm: TileMapLayer, coord: Vector2i, ts_meta: Dictionary, types: Dictionary, cache: Array) -> void:
  273. var type = types[coord]
  274. if type < TileCategory.EMPTY or type >= ts_meta.terrains.size():
  275. return
  276. var placement
  277. var terrain = _get_cache_terrain(ts_meta, type)
  278. if terrain[2] in [TerrainType.MATCH_TILES, TerrainType.DECORATION]:
  279. placement = _update_tile_tiles(tm, coord, types, cache, true)
  280. elif terrain[2] == TerrainType.MATCH_VERTICES:
  281. placement = _update_tile_vertices(tm, coord, types, cache)
  282. else:
  283. return
  284. if placement:
  285. tm.set_cell(coord, placement[0], placement[1], placement[2])
  286. func _update_tile_deferred(tm: TileMapLayer, coord: Vector2i, ts_meta: Dictionary, types: Dictionary, cache: Array):
  287. var type = types[coord]
  288. if type >= TileCategory.EMPTY and type < ts_meta.terrains.size():
  289. var terrain = _get_cache_terrain(ts_meta, type)
  290. if terrain[2] in [TerrainType.MATCH_TILES, TerrainType.DECORATION]:
  291. return _update_tile_tiles(tm, coord, types, cache, terrain[2] == TerrainType.DECORATION)
  292. elif terrain[2] == TerrainType.MATCH_VERTICES:
  293. return _update_tile_vertices(tm, coord, types, cache)
  294. return null
  295. func _widen(tm: TileMapLayer, coords: Array) -> Array:
  296. var result := {}
  297. var peering_neighbors = data.get_terrain_peering_cells(tm.tile_set, TerrainType.MATCH_TILES)
  298. for c in coords:
  299. result[c] = true
  300. var neighbors = data.neighboring_coords(tm, c, peering_neighbors)
  301. for t in neighbors:
  302. result[t] = true
  303. return result.keys()
  304. func _widen_with_exclusion(tm: TileMapLayer, coords: Array, exclusion: Rect2i) -> Array:
  305. var result := {}
  306. var peering_neighbors = data.get_terrain_peering_cells(tm.tile_set, TerrainType.MATCH_TILES)
  307. for c in coords:
  308. if !exclusion.has_point(c):
  309. result[c] = true
  310. var neighbors = data.neighboring_coords(tm, c, peering_neighbors)
  311. for t in neighbors:
  312. if !exclusion.has_point(t):
  313. result[t] = true
  314. return result.keys()
  315. # Terrains
  316. ## Returns an [Array] of categories. These are the terrains in the [TileSet] which
  317. ## are marked with [enum TerrainType] of [code]CATEGORY[/code]. Each entry in the
  318. ## array is a [Dictionary] with [code]name[/code], [code]color[/code], and [code]id[/code].
  319. func get_terrain_categories(ts: TileSet) -> Array:
  320. var result := []
  321. if !ts:
  322. return result
  323. var ts_meta := _get_terrain_meta(ts)
  324. for id in ts_meta.terrains.size():
  325. var t = ts_meta.terrains[id]
  326. if t[2] == TerrainType.CATEGORY:
  327. result.push_back({name = t[0], color = t[1], id = id})
  328. return result
  329. ## Adds a new terrain to the [TileSet]. Returns [code]true[/code] if this is successful.
  330. ## [br][br]
  331. ## [code]type[/code] must be one of [enum TerrainType].[br]
  332. ## [code]categories[/code] is an indexed list of terrain categories that this terrain
  333. ## can match as. The indexes must be valid terrains of the CATEGORY type.
  334. ## [code]icon[/code] is a [Dictionary] with either a [code]path[/code] string pointing
  335. ## to a resource, or a [code]source_id[/code] [int] and a [code]coord[/code] [Vector2i].
  336. ## The former takes priority if both are present.
  337. func add_terrain(ts: TileSet, name: String, color: Color, type: int, categories: Array = [], icon: Dictionary = {}) -> bool:
  338. if !ts or name.is_empty() or type < 0 or type == TerrainType.DECORATION or type >= TerrainType.MAX:
  339. return false
  340. var ts_meta := _get_terrain_meta(ts)
  341. # check categories
  342. if type == TerrainType.CATEGORY and !categories.is_empty():
  343. return false
  344. for c in categories:
  345. if c < 0 or c >= ts_meta.terrains.size() or ts_meta.terrains[c][2] != TerrainType.CATEGORY:
  346. return false
  347. if icon and not (icon.has("path") or (icon.has("source_id") and icon.has("coord"))):
  348. return false
  349. ts_meta.terrains.push_back([name, color, type, categories, icon])
  350. _set_terrain_meta(ts, ts_meta)
  351. _purge_cache(ts)
  352. return true
  353. ## Removes the terrain at [code]index[/code] from the [TileSet]. Returns [code]true[/code]
  354. ## if the deletion is successful.
  355. func remove_terrain(ts: TileSet, index: int) -> bool:
  356. if !ts or index < 0:
  357. return false
  358. var ts_meta := _get_terrain_meta(ts)
  359. if index >= ts_meta.terrains.size():
  360. return false
  361. if ts_meta.terrains[index][2] == TerrainType.CATEGORY:
  362. for t in ts_meta.terrains:
  363. t[3].erase(index)
  364. for s in ts.get_source_count():
  365. var source := ts.get_source(ts.get_source_id(s)) as TileSetAtlasSource
  366. if !source:
  367. continue
  368. for t in source.get_tiles_count():
  369. var coord := source.get_tile_id(t)
  370. for a in source.get_alternative_tiles_count(coord):
  371. var alternate := source.get_alternative_tile_id(coord, a)
  372. var td := source.get_tile_data(coord, alternate)
  373. var td_meta := _get_tile_meta(td)
  374. if td_meta.type == TileCategory.NON_TERRAIN:
  375. continue
  376. if td_meta.type == index:
  377. _set_tile_meta(ts, td, null)
  378. continue
  379. if td_meta.type > index:
  380. td_meta.type -= 1
  381. for peering in td_meta.keys():
  382. if !(peering is int):
  383. continue
  384. var fixed_peering = []
  385. for p in td_meta[peering]:
  386. if p < index:
  387. fixed_peering.append(p)
  388. elif p > index:
  389. fixed_peering.append(p - 1)
  390. if fixed_peering.is_empty():
  391. td_meta.erase(peering)
  392. else:
  393. td_meta[peering] = fixed_peering
  394. _set_tile_meta(ts, td, td_meta)
  395. ts_meta.terrains.remove_at(index)
  396. _set_terrain_meta(ts, ts_meta)
  397. _purge_cache(ts)
  398. return true
  399. ## Returns the number of terrains in the [TileSet].
  400. func terrain_count(ts: TileSet) -> int:
  401. if !ts:
  402. return 0
  403. var ts_meta := _get_terrain_meta(ts)
  404. return ts_meta.terrains.size()
  405. ## Retrieves information about the terrain at [code]index[/code] in the [TileSet].
  406. ## [br][br]
  407. ## Returns a [Dictionary] describing the terrain. If it succeeds, the key [code]valid[/code]
  408. ## will be set to [code]true[/code]. Other keys are [code]name[/code], [code]color[/code],
  409. ## [code]type[/code] (a [enum TerrainType]), [code]categories[/code] which is
  410. ## an [Array] of category type terrains that this terrain matches as, and
  411. ## [code]icon[/code] which is a [Dictionary] with a [code]path[/code] [String] or
  412. ## a [code]source_id[/code] [int] and [code]coord[/code] [Vector2i]
  413. func get_terrain(ts: TileSet, index: int) -> Dictionary:
  414. if !ts or index < TileCategory.EMPTY:
  415. return {valid = false}
  416. var ts_meta := _get_terrain_meta(ts)
  417. if index >= ts_meta.terrains.size():
  418. return {valid = false}
  419. var terrain := _get_cache_terrain(ts_meta, index)
  420. return {
  421. id = index,
  422. name = terrain[0],
  423. color = terrain[1],
  424. type = terrain[2],
  425. categories = terrain[3].duplicate(),
  426. icon = terrain[4].duplicate(),
  427. valid = true
  428. }
  429. ## Updates the details of the terrain at [code]index[/code] in [TileSet]. Returns
  430. ## [code]true[/code] if this succeeds.
  431. ## [br][br]
  432. ## If supplied, the [code]categories[/code] must be a list of indexes to other [code]CATEGORY[/code]
  433. ## type terrains.
  434. ## [code]icon[/code] is a [Dictionary] with either a [code]path[/code] string pointing
  435. ## to a resource, or a [code]source_id[/code] [int] and a [code]coord[/code] [Vector2i].
  436. func set_terrain(ts: TileSet, index: int, name: String, color: Color, type: int, categories: Array = [], icon: Dictionary = {valid = false}) -> bool:
  437. if !ts or name.is_empty() or index < 0 or type < 0 or type == TerrainType.DECORATION or type >= TerrainType.MAX:
  438. return false
  439. var ts_meta := _get_terrain_meta(ts)
  440. if index >= ts_meta.terrains.size():
  441. return false
  442. if type == TerrainType.CATEGORY and !categories.is_empty():
  443. return false
  444. for c in categories:
  445. if c < 0 or c == index or c >= ts_meta.terrains.size() or ts_meta.terrains[c][2] != TerrainType.CATEGORY:
  446. return false
  447. var icon_valid = icon.get("valid", "true")
  448. if icon_valid:
  449. match icon:
  450. {}, {"path"}, {"source_id", "coord"}: pass
  451. _: return false
  452. if type != TerrainType.CATEGORY:
  453. for t in ts_meta.terrains:
  454. t[3].erase(index)
  455. ts_meta.terrains[index] = [name, color, type, categories, icon]
  456. _set_terrain_meta(ts, ts_meta)
  457. _clear_invalid_peering_types(ts)
  458. _purge_cache(ts)
  459. return true
  460. ## Swaps the terrains at [code]index1[/code] and [code]index2[/code] in [TileSet].
  461. func swap_terrains(ts: TileSet, index1: int, index2: int) -> bool:
  462. if !ts or index1 < 0 or index2 < 0 or index1 == index2:
  463. return false
  464. var ts_meta := _get_terrain_meta(ts)
  465. if index1 >= ts_meta.terrains.size() or index2 >= ts_meta.terrains.size():
  466. return false
  467. for t in ts_meta.terrains:
  468. var has1 = t[3].has(index1)
  469. var has2 = t[3].has(index2)
  470. if has1 and !has2:
  471. t[3].erase(index1)
  472. t[3].push_back(index2)
  473. elif has2 and !has1:
  474. t[3].erase(index2)
  475. t[3].push_back(index1)
  476. for s in ts.get_source_count():
  477. var source := ts.get_source(ts.get_source_id(s)) as TileSetAtlasSource
  478. if !source:
  479. continue
  480. for t in source.get_tiles_count():
  481. var coord := source.get_tile_id(t)
  482. for a in source.get_alternative_tiles_count(coord):
  483. var alternate := source.get_alternative_tile_id(coord, a)
  484. var td := source.get_tile_data(coord, alternate)
  485. var td_meta := _get_tile_meta(td)
  486. if td_meta.type == TileCategory.NON_TERRAIN:
  487. continue
  488. if td_meta.type == index1:
  489. td_meta.type = index2
  490. elif td_meta.type == index2:
  491. td_meta.type = index1
  492. for peering in td_meta.keys():
  493. if !(peering is int):
  494. continue
  495. var fixed_peering = []
  496. for p in td_meta[peering]:
  497. if p == index1:
  498. fixed_peering.append(index2)
  499. elif p == index2:
  500. fixed_peering.append(index1)
  501. else:
  502. fixed_peering.append(p)
  503. td_meta[peering] = fixed_peering
  504. _set_tile_meta(ts, td, td_meta)
  505. var temp = ts_meta.terrains[index1]
  506. ts_meta.terrains[index1] = ts_meta.terrains[index2]
  507. ts_meta.terrains[index2] = temp
  508. _set_terrain_meta(ts, ts_meta)
  509. _purge_cache(ts)
  510. return true
  511. # Terrain tile data
  512. ## For a tile in a [TileSet] as specified by [TileData], set the terrain associated
  513. ## with that tile to [code]type[/code], which is an index of an existing terrain.
  514. ## Returns [code]true[/code] on success.
  515. func set_tile_terrain_type(ts: TileSet, td: TileData, type: int) -> bool:
  516. if !ts or !td or type < TileCategory.NON_TERRAIN:
  517. return false
  518. var td_meta = _get_tile_meta(td)
  519. td_meta.type = type
  520. if type == TileCategory.NON_TERRAIN:
  521. td_meta = null
  522. _set_tile_meta(ts, td, td_meta)
  523. _clear_invalid_peering_types(ts)
  524. _purge_cache(ts)
  525. return true
  526. ## Returns the terrain type associated with tile specified by [TileData]. Returns
  527. ## -1 if the tile has no associated terrain.
  528. func get_tile_terrain_type(td: TileData) -> int:
  529. if !td:
  530. return TileCategory.ERROR
  531. var td_meta := _get_tile_meta(td)
  532. return td_meta.type
  533. ## For a tile represented by [TileData] [code]td[/code] in [TileSet]
  534. ## [code]ts[/code], sets [enum SymmetryType] [code]type[/code]. This controls
  535. ## how the tile is rotated/mirrored during placement.
  536. func set_tile_symmetry_type(ts: TileSet, td: TileData, type: int) -> bool:
  537. if !ts or !td or type < SymmetryType.NONE or type > SymmetryType.ALL:
  538. return false
  539. var td_meta := _get_tile_meta(td)
  540. if td_meta.type == TileCategory.NON_TERRAIN:
  541. return false
  542. td_meta.symmetry = type
  543. _set_tile_meta(ts, td, td_meta)
  544. _purge_cache(ts)
  545. return true
  546. ## For a tile [code]td[/code], returns the [enum SymmetryType] which that
  547. ## tile uses.
  548. func get_tile_symmetry_type(td: TileData) -> int:
  549. if !td:
  550. return SymmetryType.NONE
  551. var td_meta := _get_tile_meta(td)
  552. return td_meta.get("symmetry", SymmetryType.NONE)
  553. ## Returns an Array of all [TileData] tiles included in the specified
  554. ## terrain [code]type[/code] for the [TileSet] [code]ts[/code]
  555. func get_tiles_in_terrain(ts: TileSet, type: int) -> Array[TileData]:
  556. var result:Array[TileData] = []
  557. if !ts or type < TileCategory.EMPTY:
  558. return result
  559. var cache := _get_cache(ts)
  560. if type > cache.size():
  561. return result
  562. var tiles = cache[type]
  563. if !tiles:
  564. return result
  565. for c in tiles:
  566. if c[0] < 0:
  567. continue
  568. var source := ts.get_source(c[0]) as TileSetAtlasSource
  569. var td := source.get_tile_data(c[1], c[2])
  570. result.push_back(td)
  571. return result
  572. ## Returns an [Array] of [Dictionary] items including information about each
  573. ## tile included in the specified terrain [code]type[/code] for
  574. ## the [TileSet] [code]ts[/code]. Each Dictionary item includes
  575. ## [TileSetAtlasSource] [code]source[/code], [TileData] [code]td[/code],
  576. ## [Vector2i] [code]coord[/code], and [int] [code]alt_id[/code].
  577. func get_tile_sources_in_terrain(ts: TileSet, type: int) -> Array[Dictionary]:
  578. var result:Array[Dictionary] = []
  579. var cache := _get_cache(ts)
  580. var tiles = cache[type]
  581. if !tiles:
  582. return result
  583. for c in tiles:
  584. if c[0] < 0:
  585. continue
  586. var source := ts.get_source(c[0]) as TileSetAtlasSource
  587. if not source:
  588. continue
  589. var td := source.get_tile_data(c[1], c[2])
  590. result.push_back({
  591. source = source,
  592. td = td,
  593. coord = c[1],
  594. alt_id = c[2]
  595. })
  596. return result
  597. ## For a [TileSet]'s tile, specified by [TileData], add terrain [code]type[/code]
  598. ## (an index of a terrain) to match this tile in direction [code]peering[/code],
  599. ## which is of type [enum TileSet.CellNeighbor]. Returns [code]true[/code] on success.
  600. func add_tile_peering_type(ts: TileSet, td: TileData, peering: int, type: int) -> bool:
  601. if !ts or !td or peering < 0 or peering > 15 or type < TileCategory.EMPTY:
  602. return false
  603. var ts_meta := _get_terrain_meta(ts)
  604. var td_meta := _get_tile_meta(td)
  605. if td_meta.type < TileCategory.EMPTY or td_meta.type >= ts_meta.terrains.size():
  606. return false
  607. if !td_meta.has(peering):
  608. td_meta[peering] = [type]
  609. elif !td_meta[peering].has(type):
  610. td_meta[peering].append(type)
  611. else:
  612. return false
  613. _set_tile_meta(ts, td, td_meta)
  614. _purge_cache(ts)
  615. return true
  616. ## For a [TileSet]'s tile, specified by [TileData], remove terrain [code]type[/code]
  617. ## from matching in direction [code]peering[/code], which is of type [enum TileSet.CellNeighbor].
  618. ## Returns [code]true[/code] on success.
  619. func remove_tile_peering_type(ts: TileSet, td: TileData, peering: int, type: int) -> bool:
  620. if !ts or !td or peering < 0 or peering > 15 or type < TileCategory.EMPTY:
  621. return false
  622. var td_meta := _get_tile_meta(td)
  623. if !td_meta.has(peering):
  624. return false
  625. if !td_meta[peering].has(type):
  626. return false
  627. td_meta[peering].erase(type)
  628. if td_meta[peering].is_empty():
  629. td_meta.erase(peering)
  630. _set_tile_meta(ts, td, td_meta)
  631. _purge_cache(ts)
  632. return true
  633. ## For the tile specified by [TileData], return an [Array] of peering directions
  634. ## for which terrain matching is set up. These will be of type [enum TileSet.CellNeighbor].
  635. func tile_peering_keys(td: TileData) -> Array:
  636. if !td:
  637. return []
  638. var td_meta := _get_tile_meta(td)
  639. var result := []
  640. for k in td_meta:
  641. if k is int:
  642. result.append(k)
  643. return result
  644. ## For the tile specified by [TileData], return the [Array] of terrains that match
  645. ## for the direction [code]peering[/code] which should be of type [enum TileSet.CellNeighbor].
  646. func tile_peering_types(td: TileData, peering: int) -> Array:
  647. if !td or peering < 0 or peering > 15:
  648. return []
  649. var td_meta := _get_tile_meta(td)
  650. return td_meta[peering].duplicate() if td_meta.has(peering) else []
  651. ## For the tile specified by [TileData], return the [Array] of peering directions
  652. ## for the specified terrain type [code]type[/code].
  653. func tile_peering_for_type(td: TileData, type: int) -> Array:
  654. if !td:
  655. return []
  656. var td_meta := _get_tile_meta(td)
  657. var result := []
  658. var sides := tile_peering_keys(td)
  659. for side in sides:
  660. if td_meta[side].has(type):
  661. result.push_back(side)
  662. result.sort()
  663. return result
  664. # Painting
  665. ## Applies the terrain [code]type[/code] to the [TileMapLayer] for the [Vector2i]
  666. ## [code]coord[/code]. Returns [code]true[/code] if it succeeds. Use [method set_cells]
  667. ## to change multiple tiles at once.
  668. ## [br][br]
  669. ## Use terrain type -1 to erase cells.
  670. func set_cell(tm: TileMapLayer, coord: Vector2i, type: int) -> bool:
  671. if !tm or !tm.tile_set or type < TileCategory.EMPTY:
  672. return false
  673. if type == TileCategory.EMPTY:
  674. tm.erase_cell(coord)
  675. return true
  676. var cache := _get_cache(tm.tile_set)
  677. if type >= cache.size():
  678. return false
  679. if cache[type].is_empty():
  680. return false
  681. var tile = cache[type].front()
  682. tm.set_cell(coord, tile[0], tile[1], tile[2])
  683. return true
  684. ## Applies the terrain [code]type[/code] to the [TileMapLayer] for the
  685. ## [Vector2i] [code]coords[/code]. Returns [code]true[/code] if it succeeds.
  686. ## [br][br]
  687. ## Note that this does not cause the terrain solver to run, so this will just place
  688. ## an arbitrary terrain-associated tile in the given position. To run the solver,
  689. ## you must set the require cells, and then call either [method update_terrain_cell],
  690. ## [method update_terrain_cels], or [method update_terrain_area].
  691. ## [br][br]
  692. ## If you want to prepare changes to the tiles in advance, you can use [method create_terrain_changeset]
  693. ## and the associated functions.
  694. ## [br][br]
  695. ## Use terrain type -1 to erase cells.
  696. func set_cells(tm: TileMapLayer, coords: Array, type: int) -> bool:
  697. if !tm or !tm.tile_set or type < TileCategory.EMPTY:
  698. return false
  699. if type == TileCategory.EMPTY:
  700. for c in coords:
  701. tm.erase_cell(c)
  702. return true
  703. var cache := _get_cache(tm.tile_set)
  704. if type >= cache.size():
  705. return false
  706. if cache[type].is_empty():
  707. return false
  708. var tile = cache[type].front()
  709. for c in coords:
  710. tm.set_cell(c, tile[0], tile[1], tile[2])
  711. return true
  712. ## Replaces an existing tile on the [TileMapLayer] for the [Vector2i]
  713. ## [code]coord[/code] with a new tile in the provided terrain [code]type[/code]
  714. ## *only if* there is a tile with a matching set of peering sides in this terrain.
  715. ## Returns [code]true[/code] if any tiles were changed. Use [method replace_cells]
  716. ## to replace multiple tiles at once.
  717. func replace_cell(tm: TileMapLayer, coord: Vector2i, type: int) -> bool:
  718. if !tm or !tm.tile_set or type < 0:
  719. return false
  720. var cache := _get_cache(tm.tile_set)
  721. if type >= cache.size():
  722. return false
  723. if cache[type].is_empty():
  724. return false
  725. var td = tm.get_cell_tile_data(coord)
  726. if !td:
  727. return false
  728. var ts_meta := _get_terrain_meta(tm.tile_set)
  729. var categories = ts_meta.terrains[type][3]
  730. var check_types = [type] + categories
  731. for check_type in check_types:
  732. var placed_peering = tile_peering_for_type(td, check_type)
  733. for pt in get_tiles_in_terrain(tm.tile_set, type):
  734. var check_peering := tile_peering_for_type(pt, check_type)
  735. if placed_peering == check_peering:
  736. var tile = cache[type].front()
  737. tm.set_cell(coord, tile[0], tile[1], tile[2])
  738. return true
  739. return false
  740. ## Replaces existing tiles on the [TileMapLayer] for the [Vector2i]
  741. ## [code]coords[/code] with new tiles in the provided terrain [code]type[/code]
  742. ## *only if* there is a tile with a matching set of peering sides in this terrain
  743. ## for each tile.
  744. ## Returns [code]true[/code] if any tiles were changed.
  745. func replace_cells(tm: TileMapLayer, coords: Array, type: int) -> bool:
  746. if !tm or !tm.tile_set or type < 0:
  747. return false
  748. var cache := _get_cache(tm.tile_set)
  749. if type >= cache.size():
  750. return false
  751. if cache[type].is_empty():
  752. return false
  753. var ts_meta := _get_terrain_meta(tm.tile_set)
  754. var categories = ts_meta.terrains[type][3]
  755. var check_types = [type] + categories
  756. var changed = false
  757. var potential_tiles = get_tiles_in_terrain(tm.tile_set, type)
  758. for c in coords:
  759. var found = false
  760. var td = tm.get_cell_tile_data(c)
  761. if !td:
  762. continue
  763. for check_type in check_types:
  764. var placed_peering = tile_peering_for_type(td, check_type)
  765. for pt in potential_tiles:
  766. var check_peering = tile_peering_for_type(pt, check_type)
  767. if placed_peering == check_peering:
  768. var tile = cache[type].front()
  769. tm.set_cell(c, tile[0], tile[1], tile[2])
  770. changed = true
  771. found = true
  772. break
  773. if found:
  774. break
  775. return changed
  776. ## Returns the terrain type detected in the [TileMapLayer] at specified [Vector2i]
  777. ## [code]coord[/code]. Returns -1 if tile is not valid or does not contain a
  778. ## tile associated with a terrain.
  779. func get_cell(tm: TileMapLayer, coord: Vector2i) -> int:
  780. if !tm or !tm.tile_set:
  781. return TileCategory.ERROR
  782. if tm.get_cell_source_id(coord) == -1:
  783. return TileCategory.EMPTY
  784. var t := tm.get_cell_tile_data(coord)
  785. if !t:
  786. return TileCategory.NON_TERRAIN
  787. return _get_tile_meta(t).type
  788. ## Runs the tile solving algorithm on the [TileMapLayer] for the given
  789. ## [Vector2i] coordinates in the [code]cells[/code] parameter. By default,
  790. ## the surrounding cells are also solved, but this can be adjusted by passing [code]false[/code]
  791. ## to the [code]and_surrounding_cells[/code] parameter.
  792. ## [br][br]
  793. ## See also [method update_terrain_area] and [method update_terrain_cell].
  794. func update_terrain_cells(tm: TileMapLayer, cells: Array, and_surrounding_cells := true) -> void:
  795. if !tm or !tm.tile_set:
  796. return
  797. if and_surrounding_cells:
  798. cells = _widen(tm, cells)
  799. var needed_cells := _widen(tm, cells)
  800. var types := {}
  801. for c in needed_cells:
  802. types[c] = get_cell(tm, c)
  803. var ts_meta := _get_terrain_meta(tm.tile_set)
  804. var cache := _get_cache(tm.tile_set)
  805. for c in cells:
  806. _update_tile_immediate(tm, c, ts_meta, types, cache)
  807. ## Runs the tile solving algorithm on the [TileMapLayer] for the given [Vector2i]
  808. ## [code]cell[/code]. By default, the surrounding cells are also solved, but
  809. ## this can be adjusted by passing [code]false[/code] to the [code]and_surrounding_cells[/code]
  810. ## parameter. This calls through to [method update_terrain_cells].
  811. func update_terrain_cell(tm: TileMapLayer, cell: Vector2i, and_surrounding_cells := true) -> void:
  812. update_terrain_cells(tm, [cell], and_surrounding_cells)
  813. ## Runs the tile solving algorithm on the [TileMapLayer] for the given [Rect2i]
  814. ## [code]area[/code]. By default, the surrounding cells are also solved, but
  815. ## this can be adjusted by passing [code]false[/code] to the [code]and_surrounding_cells[/code]
  816. ## parameter.
  817. ## [br][br]
  818. ## See also [method update_terrain_cells].
  819. func update_terrain_area(tm: TileMapLayer, area: Rect2i, and_surrounding_cells := true) -> void:
  820. if !tm or !tm.tile_set:
  821. return
  822. # Normalize area and extend so tiles cover inclusive space
  823. area = area.abs()
  824. area.size += Vector2i.ONE
  825. var edges = []
  826. for x in range(area.position.x, area.end.x):
  827. edges.append(Vector2i(x, area.position.y))
  828. edges.append(Vector2i(x, area.end.y - 1))
  829. for y in range(area.position.y + 1, area.end.y - 1):
  830. edges.append(Vector2i(area.position.x, y))
  831. edges.append(Vector2i(area.end.x - 1, y))
  832. var additional_cells := []
  833. var needed_cells := _widen_with_exclusion(tm, edges, area)
  834. if and_surrounding_cells:
  835. additional_cells = needed_cells
  836. needed_cells = _widen_with_exclusion(tm, needed_cells, area)
  837. var types := {}
  838. for y in range(area.position.y, area.end.y):
  839. for x in range(area.position.x, area.end.x):
  840. var coord = Vector2i(x, y)
  841. types[coord] = get_cell(tm, coord)
  842. for c in needed_cells:
  843. types[c] = get_cell(tm, c)
  844. var ts_meta := _get_terrain_meta(tm.tile_set)
  845. var cache := _get_cache(tm.tile_set)
  846. for y in range(area.position.y, area.end.y):
  847. for x in range(area.position.x, area.end.x):
  848. var coord := Vector2i(x, y)
  849. _update_tile_immediate(tm, coord, ts_meta, types, cache)
  850. for c in additional_cells:
  851. _update_tile_immediate(tm, c, ts_meta, types, cache)
  852. ## For a [TileMapLayer], create a changeset that will
  853. ## be calculated via a [WorkerThreadPool], so it will not delay processing the current
  854. ## frame or affect the framerate.
  855. ## [br][br]
  856. ## The [code]paint[/code] parameter must be a [Dictionary] with keys of type [Vector2i]
  857. ## representing map coordinates, and integer values representing terrain types.
  858. ## [br][br]
  859. ## Returns a [Dictionary] with internal details. See also [method is_terrain_changeset_ready],
  860. ## [method apply_terrain_changeset], and [method wait_for_terrain_changeset].
  861. func create_terrain_changeset(tm: TileMapLayer, paint: Dictionary) -> Dictionary:
  862. # Force cache rebuild if required
  863. var _cache := _get_cache(tm.tile_set)
  864. var cells := paint.keys()
  865. var needed_cells := _widen(tm, cells)
  866. var types := {}
  867. for c in needed_cells:
  868. types[c] = paint[c] if paint.has(c) else get_cell(tm, c)
  869. var placements := []
  870. placements.resize(cells.size())
  871. var ts_meta := _get_terrain_meta(tm.tile_set)
  872. var work := func(n: int):
  873. placements[n] = _update_tile_deferred(tm, cells[n], ts_meta, types, _cache)
  874. return {
  875. "valid": true,
  876. "tilemap": tm,
  877. "cells": cells,
  878. "placements": placements,
  879. "group_id": WorkerThreadPool.add_group_task(work, cells.size(), -1, false, "BetterTerrain")
  880. }
  881. ## Returns [code]true[/code] if a changeset created by [method create_terrain_changeset]
  882. ## has finished the threaded calculation and is ready to be applied by [method apply_terrain_changeset].
  883. ## See also [method wait_for_terrain_changeset].
  884. func is_terrain_changeset_ready(change: Dictionary) -> bool:
  885. if !change.has("group_id"):
  886. return false
  887. return WorkerThreadPool.is_group_task_completed(change.group_id)
  888. ## Blocks until a changeset created by [method create_terrain_changeset] finishes.
  889. ## This is useful to tidy up threaded work in the event that a node is to be removed
  890. ## whilst still waiting on threads.
  891. ## [br][br]
  892. ## Usage example:
  893. ## [codeblock]
  894. ## func _exit_tree():
  895. ## if changeset.valid:
  896. ## BetterTerrain.wait_for_terrain_changeset(changeset)
  897. ## [/codeblock]
  898. func wait_for_terrain_changeset(change: Dictionary) -> void:
  899. if change.has("group_id"):
  900. WorkerThreadPool.wait_for_group_task_completion(change.group_id)
  901. ## Apply the changes in a changeset created by [method create_terrain_changeset]
  902. ## once it is confirmed by [method is_terrain_changeset_ready]. The changes will
  903. ## be applied to the [TileMapLayer] that the changeset was initialized with.
  904. ## [br][br]
  905. ## Completed changesets can be applied multiple times, and stored for as long as
  906. ## needed once calculated.
  907. func apply_terrain_changeset(change: Dictionary) -> void:
  908. for n in change.cells.size():
  909. var placement = change.placements[n]
  910. if placement:
  911. change.tilemap.set_cell(change.cells[n], placement[0], placement[1], placement[2])