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.
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" );
}
}