raze/source/core/textures/buildtiles.cpp
Christoph Oelckers d22bdf9dc7 - use an alias for the menu bar texture in Blood.
Direct tile access has been disabled for now, hopefully this can be buried deep in the engine for all eternity.
2020-10-06 20:05:51 +02:00

1215 lines
36 KiB
C++

/*
** buildtexture.cpp
** Handling Build textures
**
**---------------------------------------------------------------------------
** Copyright 2019 Christoph Oelckers
** All rights reserved.
**
** Redistribution and use in source and binary forms, with or without
** modification, are permitted provided that the following conditions
** are met:
**
** 1. Redistributions of source code must retain the above copyright
** notice, this list of conditions and the following disclaimer.
** 2. Redistributions in binary form must reproduce the above copyright
** notice, this list of conditions and the following disclaimer in the
** documentation and/or other materials provided with the distribution.
** 3. The name of the author may not be used to endorse or promote products
** derived from this software without specific prior written permission.
**
** THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
** IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
** OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
** IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
** INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
** NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
** THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
**---------------------------------------------------------------------------
**
**
*/
#include "files.h"
#include "zstring.h"
#include "buildtiles.h"
#include "image.h"
#include "palette.h"
#include "m_crc32.h"
#include "build.h"
#include "gamecontrol.h"
#include "palettecontainer.h"
#include "texturemanager.h"
#include "c_dispatch.h"
#include "sc_man.h"
#include "gamestruct.h"
enum
{
MAXARTFILES_BASE = 200,
MAXARTFILES_TOTAL = 220
};
BuildTiles TileFiles;
//==========================================================================
//
//
//
//==========================================================================
picanm_t tileConvertAnimFormat(int32_t const picanimraw, int* lo, int* to)
{
// Unpack a 4 byte packed anim descriptor into something more accessible
picanm_t anm;
anm.num = picanimraw & 63;
*lo = (int8_t)((picanimraw >> 8) & 255);
*to = (int8_t)((picanimraw >> 16) & 255);
anm.sf = ((picanimraw >> 24) & 15) | (picanimraw & 192);
anm.extra = (picanimraw >> 28) & 15;
return anm;
}
//==========================================================================
//
// Base class for Build tile textures
// This needs a few subclasses for different use cases.
//
//==========================================================================
int FTileTexture::CopyPixels(FBitmap* bmp, int conversion)
{
TArray<uint8_t> buffer;
auto ppix = GetRawData();
if (ppix)
{
bmp->CopyPixelData(0, 0, ppix, Width, Height, Height, 1, 0, GPalette.BaseColors);
}
return 0;
}
TArray<uint8_t> FTileTexture::CreatePalettedPixels(int conversion)
{
TArray<uint8_t> buffer(Width * Height, true);
auto p = GetRawData();
if (p) memcpy(buffer.Data(), p, buffer.Size());
else memset(buffer.Data(), 0, buffer.Size());
return buffer;
}
//==========================================================================
//
//
//
//==========================================================================
static FGameTexture* GetTileTexture(const char* name, const TArray<uint8_t>& backingstore, uint32_t offset, int width, int height)
{
auto tex = new FArtTile(backingstore, offset, width, height);
auto p = &backingstore[offset];
auto siz = width * height;
for (int i = 0; i < siz; i++, p++)
{
// move transparent color to index 0 to get in line with the rest of the texture management.
if (*p == 0) *p = 255;
else if (*p == 255) *p = 0;
}
if (tex)
{
return MakeGameTexture(new FImageTexture(tex), name, ETextureType::Any);
}
return nullptr;
}
void BuildTiles::Init()
{
Placeholder = TexMan.GameByIndex(0);
for (auto& tile : tiledata)
{
tile.texture = Placeholder;
tile.backup = Placeholder;
tile.picanm = {};
tile.RotTile = { -1,-1 };
tile.replacement = ReplacementType::Art;
tile.alphaThreshold = 0.5;
tile.h_xsize = 0;
}
}
//==========================================================================
//
//
//
//==========================================================================
void BuildTiles::AddTile(int tilenum, FGameTexture* tex, bool permap)
{
assert(!tex->GetID().isValid()); // must not be added yet.
TexMan.AddGameTexture(tex);
tiledata[tilenum].texture = tex;
if (!permap) tiledata[tilenum].backup = tex;
}
//===========================================================================
//
// AddTiles
//
// Adds all the tiles in an artfile to the texture manager.
//
//===========================================================================
void BuildTiles::AddTiles (int firsttile, TArray<uint8_t>& RawData, const char *mapname)
{
const uint8_t *tiles = RawData.Data();
// int numtiles = LittleLong(((uint32_t *)tiles)[1]); // This value is not reliable
int tilestart = LittleLong(((int *)tiles)[2]);
int tileend = LittleLong(((int *)tiles)[3]);
const uint16_t *tilesizx = &((const uint16_t *)tiles)[8];
const uint16_t *tilesizy = &tilesizx[tileend - tilestart + 1];
const uint32_t *picanm = (const uint32_t *)&tilesizy[tileend - tilestart + 1];
const uint8_t *tiledata = (const uint8_t *)&picanm[tileend - tilestart + 1];
if (firsttile != -1)
{
tileend = tileend - tilestart + firsttile;
tilestart = firsttile;
}
for (int i = tilestart; i <= tileend; ++i)
{
int pic = i - tilestart;
int width = LittleShort(tilesizx[pic]);
int height = LittleShort(tilesizy[pic]);
uint32_t anm = LittleLong(picanm[pic]);
int size = width*height;
if (width <= 0 || height <= 0) continue;
FString texname;
if (mapname) texname.Format("maptile_%s_%05d", mapname, i);
else texname.Format("#%05d", i);
auto tex = GetTileTexture(texname, RawData, uint32_t(tiledata - tiles), width, height);
AddTile(i, tex);
int leftoffset, topoffset;
this->tiledata[i].picanmbackup = this->tiledata[i].picanm = tileConvertAnimFormat(anm, &leftoffset, &topoffset);
tex->SetOffsets(leftoffset, topoffset);
tiledata += size;
}
}
//===========================================================================
//
// Replacement textures
//
//===========================================================================
void BuildTiles::AddReplacement(int picnum, const HightileReplacement& replace)
{
auto& Hightiles = tiledata[picnum].Hightiles;
for (auto& ht : Hightiles)
{
if (replace.palnum == ht.palnum && (replace.faces[1] == nullptr) == (ht.faces[1] == nullptr))
{
ht = replace;
return;
}
}
Hightiles.Push(replace);
}
void BuildTiles::DeleteReplacement(int picnum, int palnum)
{
auto& Hightiles = tiledata[picnum].Hightiles;
for (int i = Hightiles.Size() - 1; i >= 0; i--)
{
if (Hightiles[i].palnum == palnum) Hightiles.Delete(i);
}
}
//===========================================================================
//
//
//
//===========================================================================
HightileReplacement* BuildTiles::FindReplacement(int picnum, int palnum, bool skybox)
{
auto& Hightiles = tiledata[picnum].Hightiles;
for (;;)
{
for (auto& rep : Hightiles)
{
if (rep.palnum == palnum && (rep.faces[1] != nullptr) == skybox) return &rep;
}
if (!palnum || palnum >= MAXPALOOKUPS - RESERVEDPALS) break;
palnum = 0;
}
return nullptr; // no replacement found
}
//===========================================================================
//
// CountTiles
//
// Returns the number of tiles provided by an artfile
//
//===========================================================================
int CountTiles (const char *fn, const uint8_t *RawData)
{
int version = LittleLong(*(uint32_t *)RawData);
if (version != 1)
{
Printf("%s: Invalid art file version. Must be 1, got %d\n", fn, version);
return 0;
}
int tilestart = LittleLong(((uint32_t *)RawData)[2]);
int tileend = LittleLong(((uint32_t *)RawData)[3]);
if ((unsigned)tilestart >= MAXUSERTILES || (unsigned)tileend >= MAXUSERTILES)
{
Printf("%s: Invalid tilestart or tileend\n", fn);
return 0;
}
if (tileend < tilestart)
{
Printf("%s: tileend < tilestart\n", fn);
return 0;
}
return tileend >= tilestart ? tileend - tilestart + 1 : 0;
}
//===========================================================================
//
// InvalidateTile
//
//===========================================================================
void BuildTiles::InvalidateTile(int num)
{
if ((unsigned) num < MAXTILES)
{
auto tex = tiledata[num].texture;
tex->GetTexture()->SystemTextures.Clean();
for (auto &rep : tiledata[num].Hightiles)
{
for (auto &reptex : rep.faces)
{
if (reptex) reptex->GetTexture()->SystemTextures.Clean();
}
}
tiledata[num].rawCache.data.Clear();
}
}
//===========================================================================
//
// MakeCanvas
//
// Turns texture into a canvas (i.e. camera texture)
//
//===========================================================================
void BuildTiles::MakeCanvas(int tilenum, int width, int height)
{
auto canvas = ValidateCustomTile(tilenum, ReplacementType::Canvas);
canvas->SetSize(width*4, height*4);
canvas->SetDisplaySize(width, height);
canvas->GetTexture()->SetSize(width * 4, height * 4);
static_cast<FCanvasTexture*>(canvas->GetTexture())->aspectRatio = (float)width / height;
}
//===========================================================================
//
// LoadArtFile
//
// Returns the number of tiles found.
//
// let's load everything into memory on startup.
// Even for Ion Fury this will merely add 80 MB, because the engine already needs to cache the data, albeit in a compressed-per-lump form,
// so its 100MB art file will only have a partial impact on memory.
//
//===========================================================================
int BuildTiles::LoadArtFile(const char *fn, const char *mapname, int firsttile)
{
auto old = FindFile(fn);
if (old >= ArtFiles.Size()) // Do not process if already loaded.
{
FileReader fr = fileSystem.OpenFileReader(fn);
if (fr.isOpen())
{
auto artdata = fr.Read();
const uint8_t *artptr = artdata.Data();
if (artdata.Size() > 16)
{
if (memcmp(artptr, "BUILDART", 8) == 0)
{
artdata.Delete(0, 8);
}
// Only load the data if the header is present
if (CountTiles(fn, artptr) > 0)
{
auto file = new BuildArtFile;
ArtFiles.Push(file);
file->filename = fn;
file->RawData = std::move(artdata);
AddTiles(firsttile, file->RawData, mapname);
}
}
}
else
{
//Printf("%s: file not found\n", fn);
return -1;
}
}
return 0;
}
//==========================================================================
//
//
//
//==========================================================================
void BuildTiles::LoadArtSet(const char* filename)
{
for (int index = 0; index < MAXARTFILES_BASE; index++)
{
FStringf fn(filename, index);
LoadArtFile(fn, nullptr);
}
for (auto& addart : addedArt)
{
LoadArtFile(addart, nullptr);
}
}
//==========================================================================
//
// Checks if a custom tile has alredy been added to the list.
// For each tile index there may only be one replacement and its
// type may never change!
//
// All these uses will need some review further down the line so that the texture manager's content is immutable.
//
//==========================================================================
FGameTexture* BuildTiles::ValidateCustomTile(int tilenum, ReplacementType type)
{
if (tilenum < 0 || tilenum >= MAXTILES) return nullptr;
auto &td = tiledata[tilenum];
if (td.texture != td.backup) return nullptr; // no mucking around with map tiles.
auto tile = td.texture;
auto reptype = td.replacement;
if (reptype == type) return tile; // already created
if (reptype > ReplacementType::Art) return nullptr; // different custom type - cannot replace again.
FTexture* replacement = nullptr;
td.replacement = type;
if (type == ReplacementType::Writable)
{
// Creates an empty writable tile.
// Current use cases are:
// Camera textures (should be made to be creatable by the hardware renderer instead of falling back on the software renderer.)
// thumbnails for savegame and loadgame (should bypass the texture manager entirely.)
// view tilting in the software renderer (this should just use a local buffer instead of relying on the texture manager.)
// Movie playback (like thumbnails this should bypass the texture manager entirely.)
// Blood's 'lens' effect (apparently MP only) - combination of a camera texture with a distortion map - should be made a shader effect to be applied to the camera texture.
replacement = new FImageTexture(new FWritableTile);
}
else if (type == ReplacementType::Restorable)
{
// This is for modifying an existing tile.
// It only gets used for the crosshair and a few specific effects:
// A) the fire in Blood.
// B) the pin display in Redneck Rampage's bowling lanes.
// C) Exhumed's menu plus one special effect tile.
// All of these effects should probably be redone without actual texture hacking...
if (tile->GetTexelWidth() == 0 || tile->GetTexelHeight() == 0) return nullptr; // The base must have a size for this to work.
// todo: invalidate hardware textures for tile.
replacement = new FImageTexture(new FRestorableTile(tile->GetTexture()->GetImage()));
}
else if (type == ReplacementType::Canvas)
{
replacement = new FCanvasTexture(1, 1);
}
else return nullptr;
auto rep = MakeGameTexture(replacement, tile->GetName(), ETextureType::Override);
AddTile(tilenum, rep);
return rep;
}
//==========================================================================
//
// global interface
//
//==========================================================================
int32_t BuildTiles::artLoadFiles(const char* filename)
{
TileFiles.LoadArtSet(filename);
memset(gotpic, 0, sizeof(gotpic));
return 0;
}
//==========================================================================
//
// Creates a tile for displaying custom content
//
//==========================================================================
uint8_t* BuildTiles::tileCreate(int tilenum, int width, int height)
{
if (width <= 0 || height <= 0) return nullptr;
auto tex = ValidateCustomTile(tilenum, ReplacementType::Writable);
if (tex == nullptr) return nullptr;
auto wtex = static_cast<FWritableTile*>(tex->GetTexture()->GetImage());
if (!wtex->ResizeImage(width, height)) return nullptr;
tex->SetSize(width, height);
return wtex->GetRawData();
}
//==========================================================================
//
// Makes a tile writable - only used for a handful of special cases
// (todo: Investigate how to get rid of this)
//
//==========================================================================
uint8_t* BuildTiles::tileMakeWritable(int num)
{
auto tex = ValidateCustomTile(num, ReplacementType::Restorable);
auto wtex = static_cast<FWritableTile*>(tex->GetTexture()->GetImage());
return wtex ? wtex->GetRawData() : nullptr;
}
//==========================================================================
//
// Processes data from .def files into the textures
//
//==========================================================================
void BuildTiles::PostLoadSetup()
{
SetupReverseTileMap();
for (auto& tile : tiledata)
{
FGameTexture* detailTex = nullptr, * glowTex = nullptr, * normalTex = nullptr, *specTex = nullptr;
float scalex = 1.f, scaley = 1.f;
for (auto& rep : tile.Hightiles)
{
if (rep.palnum == GLOWPAL)
{
glowTex = rep.faces[0];
}
if (rep.palnum == NORMALPAL)
{
normalTex = rep.faces[0];
}
if (rep.palnum == SPECULARPAL)
{
specTex = rep.faces[0];
}
if (rep.palnum == DETAILPAL)
{
detailTex = rep.faces[0];
scalex = rep.scale.X;
scaley = rep.scale.Y;
}
}
if (!detailTex && !glowTex && !normalTex && !specTex) continue; // if there's no layers there's nothing to do.
for (auto& rep : tile.Hightiles)
{
if (rep.faces[1]) continue; // do not muck around with skyboxes (yet)
if (rep.palnum < NORMALPAL)
{
auto tex = rep.faces[0];
// Make a copy so that multiple appearances of the same texture can be handled. They will all refer to the same internal texture anyway.
tex = MakeGameTexture(tex->GetTexture(), "", ETextureType::Any);
if (glowTex) tex->SetGlowmap(glowTex->GetTexture());
if (detailTex) tex->SetDetailmap(detailTex->GetTexture());
if (normalTex) tex->SetNormalmap(normalTex->GetTexture());
if (specTex) tex->SetSpecularmap(specTex->GetTexture());
tex->SetDetailScale(scalex, scaley);
rep.faces[0] = tex;
}
}
}
}
//==========================================================================
//
// Returns checksum for a given tile
//
//==========================================================================
int32_t tileGetCRC32(int tileNum)
{
if ((unsigned)tileNum >= (unsigned)MAXTILES) return 0;
auto tile = dynamic_cast<FArtTile*>(TileFiles.tiledata[tileNum].texture->GetTexture()->GetImage()); // only consider original ART tiles.
if (!tile) return 0;
auto pixels = tile->GetRawData();
if (!pixels) return 0;
auto size = tile->GetWidth() * tile->GetHeight();
if (size == 0) return 0;
// Temporarily revert the data to its original form with 255 being transparent. Otherwise the CRC won't match.
auto p = pixels;
for (int i = 0; i < size; i++, p++)
{
// move transparent color to index 0 to get in line with the rest of the texture management.
if (*p == 0) *p = 255;
else if (*p == 255) *p = 0;
}
auto crc = crc32(0, (const Bytef*)pixels, size);
// ... and back again.
p = pixels;
for (int i = 0; i < size; i++, p++)
{
// move transparent color to index 0 to get in line with the rest of the texture management.
if (*p == 0) *p = 255;
else if (*p == 255) *p = 0;
}
return crc;
}
CCMD(tilecrc)
{
if (argv.argc() > 1)
{
int tile = atoi(argv[1]);
Printf("%d\n", tileGetCRC32(tile));
}
}
//==========================================================================
//
// Import a tile from an external image.
// This has been signifcantly altered so it may not cover everything yet.
//
//==========================================================================
int tileImportFromTexture(const char* fn, int tilenum, int alphacut, int istexture)
{
FTextureID texid = TexMan.CheckForTexture(fn, ETextureType::Any);
if (!texid.isValid()) return -1;
auto tex = TexMan.GetGameTexture(texid);
int32_t xsiz = tex->GetTexelWidth(), ysiz = tex->GetTexelHeight();
if (xsiz <= 0 || ysiz <= 0)
return -2;
TileFiles.tiledata[tilenum].backup = TileFiles.tiledata[tilenum].texture = tex;
if (istexture)
tileSetHightileReplacement(tilenum, 0, fn, (float)(255 - alphacut) * (1.f / 255.f), 1.0f, 1.0f, 1.0, 1.0, 0);
return 0;
}
//==========================================================================
//
// Copies a tile into another and optionally translates its palette.
//
//==========================================================================
void tileCopy(int tile, int source, int pal, int xoffset, int yoffset, int flags)
{
// Todo. Since I do not know if some mod needs this it's of low priority now.
// Let's get things working first.
picanm_t* picanm = nullptr;
picanm_t* sourceanm = nullptr;
int srcxo, srcyo;
FGameTexture* tex;
if (pal == -1 && tile == source)
{
// Only modify the picanm info.
tex = tileGetTexture(tile);
if (!tex) return;
picanm = &TileFiles.tiledata[tile].picanm;
sourceanm = picanm;
srcxo = tex->GetTexelLeftOffset(0);
srcyo = tex->GetTexelTopOffset(0);
}
else
{
if (source == -1) source = tile;
tex = tileGetTexture(source);
if (!tex) return;
sourceanm = &TileFiles.tiledata[source].picanm;
srcxo = tex->GetTexelLeftOffset(0);
srcyo = tex->GetTexelTopOffset(0);
TArray<uint8_t> buffer = tex->GetTexture()->Get8BitPixels(false);
if (pal != -1)
{
auto remap = lookups.getTable(pal);
for (auto& pixel : buffer)
{
pixel = remap[pixel];
}
}
tex = MakeGameTexture(new FImageTexture(new FLooseTile(buffer, tex->GetTexelWidth(), tex->GetTexelHeight())), "", ETextureType::Any);
picanm = &TileFiles.tiledata[tile].picanm;
TileFiles.AddTile(tile, tex);
}
if (xoffset != -1024) srcxo = clamp(xoffset, -128, 127);
if (yoffset != -1024) srcyo = clamp(yoffset, -128, 127);
tex->SetOffsets(srcxo, srcyo);
picanm->sf = (picanm->sf & ~PICANM_MISC_MASK) | (sourceanm->sf & PICANM_MISC_MASK) | flags;
}
//==========================================================================
//
// Clear map specific ART
//
//==========================================================================
static FString currentMapArt;
void artClearMapArt(void)
{
for (auto& td : TileFiles.tiledata)
{
td.texture = td.backup;
td.picanm = td.picanmbackup;
}
TileFiles.SetupReverseTileMap();
currentMapArt = "";
}
//==========================================================================
//
// Load map specficied ART
//
//==========================================================================
void artSetupMapArt(const char* filename)
{
if (currentMapArt.CompareNoCase(filename)) return;
currentMapArt = filename;
artClearMapArt();
FString lcfilename = filename;
lcfilename.MakeLower();
// Re-get from the texture manager if this map's tiles have already been created.
if (TileFiles.maptilesadded.Find(lcfilename) < TileFiles.maptilesadded.Size())
{
for (int i = 0; i < MAXTILES; i++)
{
FStringf name("maptile_%s_%05d", lcfilename.GetChars(), i);
auto texid = TexMan.CheckForTexture(name, ETextureType::Any);
if (texid.isValid())
{
TileFiles.tiledata[i].texture = TexMan.GetGameTexture(texid);
}
}
return;
}
TileFiles.maptilesadded.Push(lcfilename);
FStringf firstname("%s_00.art", lcfilename.GetChars());
auto fr = fileSystem.OpenFileReader(firstname);
if (!fr.isOpen()) return;
for (auto& td : TileFiles.tiledata)
{
td.picanmbackup = td.picanm;
}
for (bssize_t i = 0; i < MAXARTFILES_TOTAL - MAXARTFILES_BASE; i++)
{
FStringf fullname("%s_%02d.art", filename, i);
TileFiles.LoadArtFile(fullname, filename);
}
TileFiles.SetupReverseTileMap();
}
//==========================================================================
//
//
//
//==========================================================================
void tileDelete(int tile)
{
TileFiles.TextureToTile.Remove(tileGetTexture(tile));
TileFiles.tiledata[tile].texture = TileFiles.tiledata[tile].backup = TexMan.GameByIndex(0);
vox_undefine(tile);
md_undefinetile(tile);
tileRemoveReplacement(tile);
}
//==========================================================================
//
//
//
//==========================================================================
void tileRemoveReplacement(int tile)
{
if ((unsigned)tile >= MAXTILES) return;
TileFiles.DeleteReplacements(tile);
}
//==========================================================================
//
//
//
//==========================================================================
void tileSetDummy(int tile, int width, int height)
{
if (width == 0 || height == 0)
{
tileDelete(tile);
}
else if (width > 0 && height > 0)
{
FStringf texname("#%05d", tile);
auto dtile = MakeGameTexture(new FImageTexture(new FDummyTile(width, height)), texname, ETextureType::Any);
TileFiles.AddTile(tile, dtile);
}
}
//==========================================================================
//
//
//
//==========================================================================
int BuildTiles::findUnusedTile(void)
{
static int lastUnusedTile = MAXUSERTILES - 1;
for (; lastUnusedTile >= 0; --lastUnusedTile)
{
auto tex = tileGetTexture(lastUnusedTile);
if (!tex || !tex->isValid()) return lastUnusedTile;
}
return -1;
}
//==========================================================================
//
// fixme: This *really* needs to be done by rotating the texture coordinates,
// not by creating an entirely new texture.
// Unfortunately it's in all the wrong place in the rendering code so it
// has to wait for later.
//
//==========================================================================
int BuildTiles::tileCreateRotated(int tileNum)
{
if ((unsigned)tileNum >= MAXTILES) return tileNum;
auto tex = tileGetTexture(tileNum);
if (!tex || tex->GetTexelWidth() <= 0 || tex->GetTexelHeight() <= 0) return tileNum;
TArray<uint8_t> buffer = tex->GetTexture()->Get8BitPixels(false);
TArray<uint8_t> dbuffer(tex->GetTexelWidth() * tex->GetTexelHeight(), true);
auto src = buffer.Data();
auto dst = dbuffer.Data();
auto width = tex->GetTexelWidth();
auto height = tex->GetTexelHeight();
for (int x = 0; x < width; ++x)
{
int xofs = width - x - 1;
int yofs = height * x;
for (int y = 0; y < height; ++y)
*(dst + y * width + xofs) = *(src + y + yofs);
}
auto dtex = MakeGameTexture(new FImageTexture(new FLooseTile(dbuffer, tex->GetTexelHeight(), tex->GetTexelWidth())), "", ETextureType::Override);
int index = findUnusedTile();
bool mapart = TileFiles.tiledata[tileNum].texture != TileFiles.tiledata[tileNum].backup;
TileFiles.AddTile(index, dtex, mapart);
return index;
}
//==========================================================================
//
//
//
//==========================================================================
void BuildTiles::CloseAll()
{
ArtFiles.DeleteAndClear();
}
//==========================================================================
//
// Specifies a replacement texture for an ART tile.
//
//==========================================================================
int tileSetHightileReplacement(int picnum, int palnum, const char* filename, float alphacut, float xscale, float yscale, float specpower, float specfactor, uint8_t flags)
{
if ((uint32_t)picnum >= (uint32_t)MAXTILES) return -1;
if ((uint32_t)palnum >= (uint32_t)MAXPALOOKUPS) return -1;
auto tex = tileGetTexture(picnum);
if (tex->GetTexelWidth() <= 0 || tex->GetTexelHeight() <= 0)
{
Printf("Warning: defined hightile replacement for empty tile %d.", picnum);
return -1; // cannot add replacements to empty tiles, must create one beforehand
}
HightileReplacement replace = {};
FTextureID texid = TexMan.CheckForTexture(filename, ETextureType::Any);
if (!texid.isValid())
{
Printf("%s: Replacement for tile %d does not exist or is invalid\n", filename, picnum);
return -1;
}
replace.faces[0] = TexMan.GetGameTexture(texid);
if (replace.faces[0] == nullptr)
replace.alphacut = min(alphacut,1.f);
replace.scale = { xscale, yscale };
replace.specpower = specpower; // currently unused
replace.specfactor = specfactor; // currently unused
replace.flags = flags;
replace.palnum = (uint16_t)palnum;
TileFiles.AddReplacement(picnum, replace);
return 0;
}
//==========================================================================
//
// Define the faces of a skybox
//
//==========================================================================
int tileSetSkybox(int picnum, int palnum, const char **facenames, int flags )
{
if ((uint32_t)picnum >= (uint32_t)MAXTILES) return -1;
if ((uint32_t)palnum >= (uint32_t)MAXPALOOKUPS) return -1;
auto tex = tileGetTexture(picnum);
if (tex->GetTexelWidth() <= 0 || tex->GetTexelHeight() <= 0)
{
Printf("Warning: defined skybox replacement for empty tile %d.", picnum);
return -1; // cannot add replacements to empty tiles, must create one beforehand
}
HightileReplacement replace = {};
for (auto &face : replace.faces)
{
FTextureID texid = TexMan.CheckForTexture(*facenames, ETextureType::Any);
if (!texid.isValid())
{
Printf("%s: Skybox image for tile %d does not exist or is invalid\n", *facenames, picnum);
return -1;
}
face = TexMan.GetGameTexture(texid);
}
replace.flags = flags;
replace.palnum = (uint16_t)palnum;
TileFiles.AddReplacement(picnum, replace);
return 0;
}
//==========================================================================
//
// Remove a replacement
//
//==========================================================================
int tileDeleteReplacement(int picnum, int palnum)
{
if ((uint32_t)picnum >= (uint32_t)MAXTILES) return -1;
if ((uint32_t)palnum >= (uint32_t)MAXPALOOKUPS) return -1;
TileFiles.DeleteReplacement(picnum, palnum);
return 0;
}
//==========================================================================
//
// Copy a block of a tile.
// Only used by RR's bowling lane.
//
//==========================================================================
void tileCopySection(int tilenum1, int sx1, int sy1, int xsiz, int ysiz, int tilenum2, int sx2, int sy2)
{
int xsiz1 = tileWidth(tilenum1);
int ysiz1 = tileHeight(tilenum1);
int xsiz2 = tileWidth(tilenum2);
int ysiz2 = tileHeight(tilenum2);
if (xsiz1 > 0 && ysiz1 > 0 && xsiz2 > 0 && ysiz2 > 0)
{
auto p1 = tilePtr(tilenum1);
auto p2 = tileData(tilenum2);
if (p2 == nullptr) return; // Error: Destination is not writable.
int x1 = sx1;
int x2 = sx2;
for (int i=0; i<xsiz; i++)
{
int y1 = sy1;
int y2 = sy2;
for (int j=0; j<ysiz; j++)
{
if (x2 >= 0 && y2 >= 0 && x2 < xsiz2 && y2 < ysiz2)
{
auto src = p1[x1 * ysiz1 + y1];
if (src != TRANSPARENT_INDEX)
p2[x2 * ysiz2 + y2] = src;
}
y1++;
y2++;
if (y1 >= ysiz1) y1 = 0;
}
x1++;
x2++;
if (x1 >= xsiz1) x1 = 0;
}
}
TileFiles.InvalidateTile(tilenum2);
}
//==========================================================================
//
// Check if two tiles are the same
//
//==========================================================================
bool tileEqualTo(int me, int other)
{
auto tile = tileGetTexture(me);
auto tile2 = tileGetTexture(other);
int tilew1 = tile->GetTexelWidth();
int tileh1 = tile->GetTexelHeight();
int tilew2 = tile2->GetTexelWidth();
int tileh2 = tile2->GetTexelHeight();
if (tilew1 == tilew2 && tileh1 == tileh2)
{
auto p1 = tileRawData(me);
auto p2 = tileRawData(other);
if (p1 && p2 && !memcmp(p1, p2, tilew1 * tileh2))
return true;
}
return false;
}
//===========================================================================
//
// Picks a texture for rendering for a given tilenum/palette combination
//
//===========================================================================
bool PickTexture(int picnum, FGameTexture* tex, int paletteid, TexturePick& pick)
{
if (!tex) tex = tileGetTexture(picnum);
if (picnum == -1) picnum = TileFiles.GetTileIndex(tex); // Allow getting replacements also when the texture is not passed by its tile number.
if (!tex->isValid() || tex->GetTexelWidth() <= 0 || tex->GetTexelHeight() <= 0) return false;
int usepalette = GetTranslationType(paletteid) - Translation_Remap;
int usepalswap = GetTranslationIndex(paletteid);
int TextureType = hw_int_useindexedcolortextures && picnum >= 0 ? TT_INDEXED : TT_TRUECOLOR;
pick.translation = paletteid;
pick.basepalTint = 0xffffff;
auto& h = lookups.tables[usepalswap];
bool applytint = false;
// Canvas textures must be treated like hightile replacements in the following code.
if (picnum < 0) picnum = TileFiles.GetTileIndex(tex); // Allow getting replacements also when the texture is not passed by its tile number.
auto rep = (picnum >= 0 && hw_hightile && !(h.tintFlags & TINTF_ALWAYSUSEART)) ? TileFiles.FindReplacement(picnum, usepalswap) : nullptr;
if (rep || tex->GetTexture()->isHardwareCanvas())
{
if (usepalette != 0)
{
// This is a global setting for the entire scene, so let's do it here, right at the start. (Fixme: Store this in a static table instead of reusing the same entry for all palettes.)
auto& hh = lookups.tables[MAXPALOOKUPS - 1];
// This sets a tinting color for global palettes, e.g. water or slime - only used for hires replacements (also an option for low-resource hardware where duplicating the textures may be problematic.)
pick.basepalTint = hh.tintColor;
}
if (rep)
{
tex = rep->faces[0];
}
if (!rep || rep->palnum != usepalswap || (h.tintFlags & TINTF_APPLYOVERALTPAL)) applytint = true;
pick.translation = 0;
}
else
{
// Only look up the palette if we really want to use it (i.e. when creating a true color texture of an ART tile.)
if (TextureType == TT_TRUECOLOR)
{
if (h.tintFlags & (TINTF_ALWAYSUSEART | TINTF_USEONART))
{
applytint = true;
if (!(h.tintFlags & TINTF_APPLYOVERPALSWAP)) usepalswap = 0;
}
pick.translation = TRANSLATION(usepalette + Translation_Remap, usepalswap);
}
else pick.translation |= 0x80000000;
}
if (applytint && h.tintFlags)
{
pick.tintFlags = h.tintFlags;
pick.tintColor = h.tintColor;
}
else
{
pick.tintFlags = -1;
pick.tintColor = 0xffffff;
}
pick.texture = tex;
return true;
}
//===========================================================================
//
// Parsing stuff for tile data comes below.
//
//===========================================================================
//===========================================================================
//
// Helpers for tile parsing
//
//===========================================================================
bool ValidateTileRange(const char* cmd, int &begin, int& end, FScriptPosition pos, bool allowswap)
{
if (end < begin)
{
pos.Message(MSG_WARNING, "%s: tile range [%d..%d] is backwards. Indices were swapped.", cmd, begin, end);
std::swap(begin, end);
}
if ((unsigned)begin >= MAXUSERTILES || (unsigned)end >= MAXUSERTILES)
{
pos.Message(MSG_ERROR, "%s: Invalid tile range [%d..%d]", cmd, begin, end);
return false;
}
return true;
}
bool ValidateTilenum(const char* cmd, int tile, FScriptPosition pos)
{
if ((unsigned)tile >= MAXUSERTILES)
{
pos.Message(MSG_ERROR, "%s: Invalid tile number %d", cmd, tile);
return false;
}
return true;
}
//===========================================================================
//
// Internal worker for tileImportTexture
//
//===========================================================================
void processTileImport(const char *cmd, FScriptPosition& pos, TileImport& imp)
{
if (!ValidateTilenum(cmd, imp.tile, pos))
return;
if (imp.crc32 != INT64_MAX && int(imp.crc32) != tileGetCRC32(imp.tile))
return;
if (imp.sizex != INT_MAX && tileWidth(imp.tile) != imp.sizex && tileHeight(imp.tile) != imp.sizey)
return;
gi->SetTileProps(imp.tile, imp.surface, imp.vox, imp.shade);
if (imp.fn.IsNotEmpty() && tileImportFromTexture(imp.fn, imp.tile, imp.alphacut, imp.istexture) < 0) return;
TileFiles.tiledata[imp.tile].picanm.sf |= imp.flags;
// This is not quite the same as originally, for two reasons:
// 1: Since these are texture properties now, there's no need to clear them.
// 2: The original code assumed that an imported texture cannot have an offset. But this can import Doom patches and PNGs with grAb, so the situation is very different.
if (imp.xoffset == INT_MAX) imp.xoffset = tileLeftOffset(imp.tile);
if (imp.yoffset == INT_MAX) imp.yoffset = tileTopOffset(imp.tile);
auto tex = tileGetTexture(imp.tile);
if (tex) tex->SetOffsets(imp.xoffset, imp.yoffset);
if (imp.extra != INT_MAX) TileFiles.tiledata[imp.tile].picanm.extra = imp.extra;
}
//===========================================================================
//
// Internal worker for tileSetAnim
//
//===========================================================================
void processSetAnim(const char* cmd, FScriptPosition& pos, SetAnim& imp)
{
if (!ValidateTilenum(cmd, imp.tile1, pos) ||
!ValidateTilenum(cmd, imp.tile2, pos))
return;
if (imp.type < 0 || imp.type > 3)
{
pos.Message(MSG_ERROR, "%s: animation type must be 0-3, got %d", cmd, imp.type);
return;
}
int count = imp.tile2 - imp.tile1;
if (imp.type == (PICANM_ANIMTYPE_BACK >> PICANM_ANIMTYPE_SHIFT) && imp.tile1 > imp.tile2)
count = -count;
TileFiles.setAnim(imp.tile1, imp.type, imp.speed, count);
}
TileSiz tilesiz;
PicAnm picanm;
#if 0 // this only gets in if unavoidable. It'd be preferable if the script side can solely operate on texture names.
#include "vm.h"
static int GetTexture(int tile, int anim)
{
if (tile < 0 || tile >= MAXTILES) return 0;
auto tex = tileGetTexture(tile, anim);
return tex ? tex->GetID().GetIndex() : 0;
}
DEFINE_ACTION_FUNCTION_NATIVE(_TileFiles, GetTexture, GetTexture)
{
PARAM_PROLOGUE;
PARAM_INT(tile);
PARAM_BOOL(animate);
ACTION_RETURN_INT(GetTexture(tile, animate));
}
#endif