Room Images and Tileset Override

How the room scene images upload works and how the overrideSceneImagesWithMapFile option keeps the room scene_images field in sync with the Tiled map file tilesets.

Override tilesets per room

Each room has its own Tiled map and its own tileset images. By default the images of a room are taken from the tilesets listed in its map, so uploading the map and its images is all a room needs.

  1. Log in to the administration panel at /reldens-admin (the path can be changed with the RELDENS_ADMIN_ROUTE_PATH environment variable).
  2. Open Rooms > Rooms (/reldens-admin/rooms). The list shows the Map Filename and the Scene Images of every room.
  3. Click Create New for a new room, or the edit icon of an existing room.
  4. In Map Filename choose the Tiled map JSON file.
  5. In Scene Images click Choose Files and select every tileset image used by that map (several files can be selected at once). The file names must match the image of each tileset in the map.
  6. Click Save. Reldens reads the tilesets of the map and replaces the Scene Images with exactly the images the map uses, as long as all of them were uploaded. Images the map does not use are dropped from the field.
  7. When you edit the room again, the images used by the map tilesets have no remove button and show an alert icon, the other images can still be removed with the X button.
  8. To manage the images by hand, open Settings > Config, click Create New, fill Scope server, Path rooms/maps/overrideSceneImagesWithMapFile, Value 0, Type boolean, save and restart the server.
Admin Panel - Rooms list with the map filename and scene images of each room Admin Panel - Create new room form with the map file and scene images upload fields

Configuration reference

  • Map Filename (room field, required) - the Tiled map JSON file, a single upload.
  • Scene Images (room field, required) - the tileset images of the map, a multiple upload saved as a comma separated list.
  • server / rooms/maps/overrideSceneImagesWithMapFile (config, boolean, not installed) - when missing or 1 the scene images always follow the map tilesets, 0 lets you manage them by hand.

Code integration (advanced)

Overview

This page explains how the room scene images upload system works in the administration panel and how the overrideSceneImagesWithMapFile option automatically synchronizes the scene images with the Tiled map file tilesets.

Configuration

  • Config path: server/rooms/maps/overrideSceneImagesWithMapFile
  • Type: boolean
  • Default: true
  • Location: database config table, as scope server plus path rooms/maps/overrideSceneImagesWithMapFile (read through ConfigManager.getWithoutLogs; there is no environment variable override for config paths). No row is seeded, so the default applies until one is added.

When enabled, the system uses the Tiled map file as the source of truth for scene images, automatically overriding the scene_images field with the images listed in the map's tilesets.

File Locations

Source code

  • Validator: lib/admin/server/room-map-tilesets-validator.js
  • Subscriber: lib/admin/server/subscribers/rooms-entity-subscriber.js
  • File upload renderer: lib/admin/server/rooms-file-upload-renderer.js
  • Admin plugin: lib/admin/server/plugin.js

Admin interface

  • Tileset file item template: theme/admin/templates/fields/edit/tileset-file-item.html
  • Tileset alert wrapper template: theme/admin/templates/fields/edit/tileset-alert-wrapper.html
  • Client JS: theme/admin/js/reldens-admin-client-maps.js (AdminClientMaps.bindTilesetAlertIcons())
  • Client CSS: theme/admin/css/component-entries.css (.tileset-alert-wrapper, .upload-files-with-alert), imported from theme/admin/css/reldens-admin-client.css
  • Router: @reldens/cms package, lib/admin-manager/router-contents.js

Database Schema

Rooms table

  • id - room identifier
  • map_filename - Tiled map JSON file (e.g. reldens-forest-level-1.json)
  • scene_images - comma-separated list of tileset images (e.g. reldens-forest-level-1.png,reldens-new-age-town.png)

Upload configuration

Both fields are configured as upload fields in lib/rooms/server/entities/rooms-entity-override.js, and BOTH use the SAME bucket, <projectThemePath>/assets/maps (e.g. theme/default/assets/maps), with bucketPath /assets/maps/:

  • map_filename - single file upload, allowedTypes: TEXT
  • scene_images - multiple file upload (isArray: ','), allowedTypes: IMAGE

System Flow

1. Initial room creation

User actions:

  1. Navigate to Admin > Rooms > Create New.
  2. Upload the map JSON file to the map_filename field.
  3. Upload the tileset images to the scene_images field.
  4. Click Save.

System processing:

  1. Upload phase - files saved to their bucket.
  2. Validation phase - validateUploadedFiles() checks the required fields.
  3. Save phase - entity created in the database.
  4. Post-save event - reldens.adminAfterEntitySave fires.
  5. Validator execution - RoomMapTilesetsValidator.validate() runs.

Validator logic:

// Check if override is enabled
overrideEnabled = config.getWithoutLogs('server/rooms/maps/overrideSceneImagesWithMapFile', true)

// Read map file
mapData = readMapFile(bucket, mapFilename, roomId)

// Extract tileset images from map JSON
tilesetImages = extractTilesetImages(mapData)
// Example: ['reldens-forest-level-1.png']

// Compare with current scene_images
if (tilesetImages !== currentSceneImages) {
    // Validate all images exist in scene_images bucket
    if (validateImagesExist(tilesetImages, sceneImagesBucket)) {
        // Override scene_images with tileset images
        roomsRepository.updateById(roomId, {scene_images: tilesetImages.join(',')})
    }
}

2. Room editing

User actions:

  1. Navigate to Admin > Rooms > Edit Room.
  2. View the existing files in both fields.
  3. Modify the files or click Save without changes.

The edit form is populated on the reldens.adminEditPropertiesPopulation event:

// 1. Event emitted with room data
event = {
    // Entity configuration
    driverResource,
    renderedEditProperties, // Form properties
    // Room from database
    loadedEntity,
    entityId: 'rooms',
    entityData: loadedEntity
}

// 2. RoomsEntitySubscriber.populateEditFormTilesetImages() executes
if (overrideSceneImagesWithMapFile) {
    // Extract tileset images from map file
    tilesetImages = validator.extractTilesetImagesFromEntity(entityData, driverResource)

    // Inject into form properties
    renderedEditProperties.tilesetImages = tilesetImages
    renderedEditProperties.overrideSceneImagesEnabled = true
}

// 3. RoomsFileUploadRenderer processes scene_images field
// Event: reldens.adminBeforeFieldRender
if (propertyKey === 'scene_images' && tilesetImages.length > 0) {
    // Render each file with protection flag
    for each file:
        renderedFileItems.push(render tileset-file-item.html with {
            filename,
            isProtected: tilesetImages.includes(filename)
        })

    // Wrap files in alert container
    templateData.renderedFiles = render tileset-alert-wrapper.html
}

// 4. Template renders with tileset protection
{{^isProtected}}
    <button class="remove-upload-btn">X</button> -
{{/isProtected}}
{{filename}}

Result:

  • Protected images (tilesets): NO remove button.
  • Non-protected images: remove button shown.
  • An alert icon displays with an info message.

3. Saving changes

Scenario A: no files changed

  1. User clicks Save without uploading or removing files.
  2. Validation passes (the existing files satisfy the requirement).
  3. Entity updated with the form data.
  4. Post-save validator runs.
  5. If scene_images matches the tilesets: no action.
  6. If there is a mismatch: overridden with the tileset images.

Scenario B: add a new image

  1. User uploads an additional image to scene_images.
  2. prepareUploadPatchData() appends the new file to the existing files.
  3. Entity saved with existing_images.png,new_image.png.
  4. Post-save validator runs.
  5. Validates the tileset images exist.
  6. Overrides scene_images with ONLY the tileset images (removes the non-tileset images).

Scenario C: remove a non-protected image

  1. User clicks the X button on a non-protected image.
  2. The client adds the filename to the removed_scene_images hidden input.
  3. prepareUploadPatchData() filters the removed files.
  4. Entity saved with the filtered list.
  5. Post-save validator runs.
  6. Overrides with the tileset images (removes the non-tileset files).

Scenario D: attempt to remove a protected image (prevented)

  1. The protected image (tileset) has NO remove button.
  2. The user cannot remove it through the UI.
  3. The alert icon displays: "Images specified in the tileset can't be removed since the option overrideSceneImagesWithMapFile is active."

4. Map file update

  1. User replaces map_filename with a new Tiled map.
  2. The new map references different tileset images.
  3. Entity saved.
  4. Post-save validator executes.
  5. Reads the new map file tilesets.
  6. Replaces scene_images with the new tileset images.
  7. The old images are no longer referenced (the user must manage the cleanup).

Technical Details

Map file structure

Example: reldens-forest-level-1.json

{
    "tilesets": [
        {
            "columns": 25,
            "firstgid": 1,
            "image": "reldens-forest-level-1.png",
            "imageheight": 782,
            "imagewidth": 850,
            "name": "reldens-forest-level-1",
            "tilecount": 564
        }
    ]
}

Extraction logic, strips any path prefix and removes duplicates:

extractTilesetImages(mapData) {
    let tilesets = mapData.tilesets || []
    let images = []

    for (let tileset of tilesets) {
        let tilesetImage = tileset.image  // 'reldens-forest-level-1.png' or '../images/reldens-forest-level-1.png'
        let imageFileName = tilesetImage.split('/').pop()  // Extract filename only

        if (!images.includes(imageFileName)) {
            images.push(imageFileName)
        }
    }

    return images  // ['reldens-forest-level-1.png']
}

Validation logic

Array comparison (validator):

arraysAreEqual(array1, array2) {
    if (array1.length !== array2.length) {
        return false
    }
    let sorted1 = [...array1].sort()
    let sorted2 = [...array2].sort()
    for (let i = 0; i < sorted1.length; i++) {
        if (sorted1[i] !== sorted2[i]) {
            return false
        }
    }
    return true
}

Image existence validation (validator):

validateImagesExist(tilesetImages, sceneImagesBucket, roomId, mapFilename) {
    for (let imageFileName of tilesetImages) {
        let imageFilePath = FileHandler.joinPaths(sceneImagesBucket, imageFileName)

        if (!FileHandler.exists(imageFilePath)) {
            return false
        }
    }

    return true
}

Client-side protection

File item template (tileset-file-item.html):

<p class="upload-current-file" data-field="{{&fieldName}}" data-filename="{{&filename}}">
    {{^isProtected}}
        <button type="button" class="remove-upload-btn" data-field="{{&fieldName}}" data-filename="{{&filename}}" title="REMOVE">X</button> -
    {{/isProtected}}
    {{&filename}}
</p>

Alert wrapper template (tileset-alert-wrapper.html):

<div class="tileset-alert-wrapper">
    <div class="upload-files-with-alert">
        {{{renderedFileItems}}}
    </div>
    <div class="alert-icon-container">
        <img src="/assets/admin/alert.png" class="alert-icon" alt="Info" title="Images specified in the tileset can't be removed since the option overrideSceneImagesWithMapFile is active.">
        <span class="tileset-info-message hidden">Images specified in the tileset can't be removed since the option overrideSceneImagesWithMapFile is active.</span>
    </div>
</div>

JavaScript toggle (AdminClientMaps.bindTilesetAlertIcons() in theme/admin/js/reldens-admin-client-maps.js):

for (let icon of document.querySelectorAll('.alert-icon')) {
    icon.addEventListener('click', () => {
        let message = icon.nextElementSibling
        if (message?.classList.contains('tileset-info-message')) {
            message.classList.toggle('hidden')
        }
    })
}

Benefits

  • Consistency: scene images always match the map tilesets.
  • Automation: no manual sync between the map and the images.
  • Single source of truth: the Tiled map file controls the image references.
  • Developer experience: edit maps in Tiled, changes auto-sync.

Limitations

  • One-way sync: map to database only (not bidirectional).
  • Cleanup required: removing a tileset from the map does not delete the old image files.
  • Override always wins: manual changes to scene_images get overwritten on the next save.
  • Active by default: the key is not seeded in the config table and both readers default to true, so the override runs until a row with value 0 is added.

Disabling the Feature

The feature is active by default: no config row is seeded for it, and both RoomMapTilesetsValidator.validate() (lib/admin/server/room-map-tilesets-validator.js) and RoomsEntitySubscriber.populateEditFormTilesetImages() (lib/admin/server/subscribers/rooms-entity-subscriber.js) read it with true as the fallback:

let overrideEnabled = this.config.getWithoutLogs('server/rooms/maps/overrideSceneImagesWithMapFile', true);

To disable the tileset override and manage the images manually, add the config row. The key is read only from the config table, the path column excludes the server scope, and type 3 is boolean in config_types:

INSERT INTO config (scope, path, value, type) VALUES ('server', 'rooms/maps/overrideSceneImagesWithMapFile', '0', 3);

If the row already exists, update it instead:

UPDATE config SET value = '0' WHERE scope = 'server' AND path = 'rooms/maps/overrideSceneImagesWithMapFile';

The config table is loaded into ConfigManager at startup, so a row added or changed by SQL takes effect after a server restart.

Result:

  • Post-save validation skipped.
  • All images show remove buttons.
  • Full manual control over the scene_images field.
  • The map file and scene_images can diverge.

Related Documentation

Go Up