Tileset/TilesetResource.cs

Resource class representing a 2D tileset asset. It stores tiles (TileDefinition), image path, tile sizing and separation, tag sets, provides texture loading/caching, tile generation from a grid, lookup utilities, and handles GUIDs and internal bookkeeping.

File Access
using Sandbox;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;

namespace Saandy.Tilemapper;

[AssetType( Name = "2D Tileset", Extension = "tileset", Category = "Tilemapper" )]
public partial class TilesetResource : GameResource
{
	[Property, ImageAssetPath, Title( "Tileset Image" ), Group( "Tileset Setup" )]
	public string FilePath { get; set; }

	[Property, Group( "Tileset Setup" )]
	public Vector2Int TileSize { get; set; } = new Vector2Int( 16, 16 );

	[Property, Group( "Tileset Setup" )]
	public Vector2Int TileSeparation { get; set; } = Vector2Int.Zero;

	[Property, Group( "Tiles" )]
	public List<TileDefinition> Tiles { get; set; } = new();

	[Property, Group( "Tags" ), Title( "Global Tags" )]
	public TagSet GlobalTags { get; set; } = new();

	[Property, Group( "Brush" )]
	public BrushType BrushType { get; set; } = BrushType.Manual;

	[Property, Hide]
	public Guid TilesetGuid { get; set; }

	[Property, Group( "Brush" ), Title( "Manual Tile Index" )]
	public ushort ManualTileIndex { get; set; } = 0;

	[JsonIgnore, Hide]
	internal Dictionary<Guid, TileDefinition> TileMap { get; set; } = new();

	[JsonIgnore, Hide]
	private Texture _cachedTexture;

	[JsonIgnore, Hide]
	private Vector2Int _cachedTextureSize = Vector2Int.Zero;

	public Texture GetTexture()
	{
		if ( string.IsNullOrWhiteSpace( FilePath ) )
			return null;

		if ( _cachedTexture == null || !_cachedTexture.IsValid )
		{
			_cachedTexture = Texture.Load( FilePath );

			if ( _cachedTexture != null && _cachedTexture.IsValid )
			{
				_cachedTextureSize = new Vector2Int(
					_cachedTexture.Width,
					_cachedTexture.Height
				);
			}
		}

		return _cachedTexture;
	}

	public Vector2Int GetTextureSize()
	{
		var texture = GetTexture();

		if ( texture != null && texture.IsValid )
			return _cachedTextureSize;

		return Vector2Int.One;
	}

	public void ClearTextureCache()
	{
		_cachedTexture = null;
		_cachedTextureSize = Vector2Int.Zero;
	}

	public bool EnsureTilesetGuid()
	{
		if ( TilesetGuid != Guid.Empty )
			return false;

		RegenerateTilesetGuid();
		return true;
	}

	public void RegenerateTilesetGuid()
	{
		TilesetGuid = Guid.NewGuid();
		StateHasChanged();
	}

	public Guid GetTilesetGuid()
	{
		EnsureTilesetGuid();
		return TilesetGuid;
	}

	public void AddTile( TileDefinition tile )
	{
		Tiles ??= new();

		if ( tile == null )
			return;

		if ( tile.Id == Guid.Empty )
			tile.Id = Guid.NewGuid();

		tile.Tileset = this;

		Tiles.Add( tile );

		InternalUpdateTiles();
	}

	public void RemoveTile( TileDefinition tile )
	{
		if ( tile == null )
			return;

		Tiles ??= new();

		TileMap?.Remove( tile.Id );
		Tiles.Remove( tile );

		InternalUpdateTiles();
	}

	public void RemoveTileAt( int index )
	{
		Tiles ??= new();

		if ( index < 0 || index >= Tiles.Count )
			return;

		Tiles.RemoveAt( index );
		ClampManualTileIndex();

		InternalUpdateTiles();
	}

	public void ClearTiles()
	{
		Tiles ??= new();
		Tiles.Clear();
		ManualTileIndex = 0;

		InternalUpdateTiles();
	}

	public void ReorderTiles( List<TileDefinition> orderedTiles )
	{
		if ( orderedTiles == null )
			return;

		Tiles ??= new();

		var final = new List<TileDefinition>();

		// First add the requested order.
		foreach ( var tile in orderedTiles )
		{
			if ( tile == null )
				continue;

			if ( final.Contains( tile ) )
				continue;

			final.Add( tile );
		}

		// Then append anything not assigned to the template.
		foreach ( var tile in Tiles )
		{
			if ( tile == null )
				continue;

			if ( final.Contains( tile ) )
				continue;

			final.Add( tile );
		}

		Tiles.Clear();
		Tiles.AddRange( final );

		InternalUpdateTiles();
	}

	public Vector4 GetUvRect( ushort tileId )
	{
		if ( Tiles == null || Tiles.Count == 0 )
			return new Vector4( 0, 0, 1, 1 );

		int index = Math.Clamp( tileId, 0, Tiles.Count - 1 );

		var tile = Tiles[index];

		if ( tile == null )
			return new Vector4( 0, 0, 1, 1 );

		tile.Tileset = this;

		return tile.GetUvRect();
	}

	public TileDefinition GetTileDefinition( ushort tileId )
	{
		Tiles ??= new();

		if ( Tiles.Count == 0 )
			return null;

		int index = Math.Clamp( tileId, 0, Tiles.Count - 1 );
		var tile = Tiles[index];

		if ( tile != null )
		{
			tile.Tileset = this;
			tile.Tags ??= new TagSet();
		}

		return tile;
	}

	public TagSet GetCombinedTags( ushort tileId )
	{
		EnsureTagSets();

		var result = new TagSet();
		CopyTagsTo( GlobalTags, result );

		var tile = GetTileDefinition( tileId );
		if ( tile != null )
			CopyTagsTo( tile.Tags, result );

		return result;
	}

	public bool HasGlobalTag( string tag )
	{
		EnsureTagSets();
		return !string.IsNullOrWhiteSpace( tag ) && GlobalTags.Has( tag );
	}

	public bool HasTileTag( ushort tileId, string tag, bool includeGlobalTags = true )
	{
		if ( string.IsNullOrWhiteSpace( tag ) )
			return false;

		EnsureTagSets();

		if ( includeGlobalTags && GlobalTags.Has( tag ) )
			return true;

		var tile = GetTileDefinition( tileId );
		return tile?.Tags != null && tile.Tags.Has( tag );
	}

	[Button( "Generate Tiles From Grid" ), Group( "Tileset Setup" )]
	public void GenerateTilesFromGrid()
	{
		Tiles ??= new();
		Tiles.Clear();

		Vector2Int textureSize = GetTextureSize();

		if ( textureSize.x <= 1 || textureSize.y <= 1 )
		{
			Log.Warning( "Cannot generate tiles. Invalid texture size." );
			return;
		}

		int tileW = Math.Max( 1, TileSize.x );
		int tileH = Math.Max( 1, TileSize.y );

		int sepX = Math.Max( 0, TileSeparation.x );
		int sepY = Math.Max( 0, TileSeparation.y );

		int stepX = tileW + sepX;
		int stepY = tileH + sepY;

		int index = 0;

		for ( int y = 0; y + tileH <= textureSize.y; y += stepY )
		{
			for ( int x = 0; x + tileW <= textureSize.x; x += stepX )
			{
				AddTile( new TileDefinition
				{
					Name = $"Tile {index}",
					SourceRect = new Rect( x, y, tileW, tileH )
				} );

				index++;
			}
		}

		InternalUpdateTiles();

		Log.Info( $"Generated {Tiles.Count} tiles." );
	}

	protected override void PostLoad()
	{
		base.PostLoad();
		InternalReload();
	}

	protected override void PostReload()
	{
		base.PostReload();

		ClearTextureCache();
		InternalReload();
	}

	private void InternalReload()
	{
		EnsureTilesetGuid();

		Tiles ??= new();

		var realTiles = new List<TileDefinition>();

		foreach ( var tile in Tiles )
		{
			if ( tile == null )
				continue;

			realTiles.Add( tile );
		}

		Tiles = realTiles;
		EnsureTagSets();

		InternalUpdateTiles();
	}

	public void InternalUpdateTiles()
	{
		EnsureTilesetGuid();
		EnsureTagSets();

		Tiles ??= new();
		ClampManualTileIndex();
		TileMap ??= new();

		TileMap.Clear();

		for ( int i = 0; i < Tiles.Count; i++ )
		{
			var tile = Tiles[i];

			if ( tile == null )
				continue;

			if ( tile.Id == Guid.Empty )
				tile.Id = Guid.NewGuid();

			if ( string.IsNullOrWhiteSpace( tile.Name ) )
				tile.Name = $"Tile {i}";

			tile.Tags ??= new TagSet();
			tile.Tileset = this;
			TileMap[tile.Id] = tile;
		}
	}

	public void EnsureTagSets()
	{
		GlobalTags ??= new TagSet();
		Tiles ??= new();

		foreach ( var tile in Tiles )
		{
			if ( tile == null )
				continue;

			tile.Tags ??= new TagSet();
		}
	}

	private static void CopyTagsTo( TagSet source, TagSet destination )
	{
		if ( source == null || destination == null )
			return;

		foreach ( string tag in source.TryGetAll() ?? Enumerable.Empty<string>() )
		{
			if ( string.IsNullOrWhiteSpace( tag ) )
				continue;

			destination.Add( tag );
		}
	}

	private void ClampManualTileIndex()
	{
		Tiles ??= new();

		if ( Tiles.Count == 0 )
		{
			ManualTileIndex = 0;
			return;
		}

		ManualTileIndex = (ushort)Math.Clamp( ManualTileIndex, 0, Tiles.Count - 1 );
	}

	protected override Bitmap CreateAssetTypeIcon( int width, int height )
	{
		return CreateSimpleAssetTypeIcon( "calendar_view_month", width, height, "#fab006", "#1a2c17" );
	}
}