mirror of
https://github.com/ZDoom/Raze.git
synced 2024-11-26 22:11:54 +00:00
462 lines
15 KiB
C++
462 lines
15 KiB
C++
/*
|
|
** tilesetbuilder.cpp
|
|
** Constructs the full tile set and adds it to the texture manager.
|
|
**
|
|
**---------------------------------------------------------------------------
|
|
** Copyright 2019-2022 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 "image.h"
|
|
#include "texturemanager.h"
|
|
#include "m_crc32.h"
|
|
#include "c_dispatch.h"
|
|
#include "tiletexture.h"
|
|
#include "tilesetbuilder.h"
|
|
#include "gamecontrol.h"
|
|
#include "startupinfo.h"
|
|
#include "printf.h"
|
|
#include "m_argv.h"
|
|
#include "gamestruct.h"
|
|
|
|
const char* G_DefFile(void);
|
|
void loaddefinitionsfile(TilesetBuildInfo& info, const char* fn, bool cumulative = false, bool maingrp = false);
|
|
|
|
|
|
//==========================================================================
|
|
//
|
|
// Returns checksum for a given tile or texture
|
|
//
|
|
//==========================================================================
|
|
|
|
int32_t tileGetCRC32(FImageSource* image)
|
|
{
|
|
if (image == nullptr) return 0;
|
|
auto pixels = image->GetPalettedPixels(0);
|
|
if (pixels.Size() == 0) return 0;
|
|
|
|
// To get proper CRCs as the calling code expects we need to put the translucent index back to 255.
|
|
for (auto& p : pixels)
|
|
{
|
|
if (p == 0) p = 255;
|
|
else if (p == 255) p = 0;
|
|
}
|
|
|
|
return crc32(0, (const Bytef*)pixels.Data(), pixels.Size());
|
|
}
|
|
|
|
CCMD(tilecrc)
|
|
{
|
|
if (argv.argc() > 1)
|
|
{
|
|
char* p;
|
|
int tile = strtol(argv[1], &p, 10);
|
|
FGameTexture* tex;
|
|
if (tile >= 0 && tile < MAXTILES && !*p)
|
|
{
|
|
tex = TexMan.GetGameTexture(tileGetTextureID(tile));
|
|
}
|
|
else
|
|
{
|
|
tex = TexMan.FindGameTexture(argv[1], ETextureType::Any);
|
|
}
|
|
auto img = tex? tex->GetTexture()->GetImage() : nullptr;
|
|
if (!img) Printf("%s: not a valid texture", argv[1]);
|
|
else Printf("%d\n", tileGetCRC32(img));
|
|
}
|
|
}
|
|
|
|
//---------------------------------------------------------------------------
|
|
//
|
|
//
|
|
//
|
|
//---------------------------------------------------------------------------
|
|
|
|
static void LoadDefinitions(TilesetBuildInfo& info)
|
|
{
|
|
for (unsigned i = 0; i < info.tile.Size(); i++)
|
|
{
|
|
if (info.tile[i].extinfo.tiletovox > info.nextvoxid) info.nextvoxid = info.tile[i].extinfo.tiletovox;
|
|
}
|
|
info.nextvoxid++;
|
|
const char* defsfile = G_DefFile();
|
|
FString razedefsfile = defsfile;
|
|
razedefsfile.Substitute(".def", "-raze.def");
|
|
|
|
loaddefinitionsfile(info, "engine/engine.def", true, true); // Internal stuff that is required.
|
|
|
|
// check what we have.
|
|
// user .defs override the default ones and are not cumulative.
|
|
// if we fine even one Raze-specific file, all of those will be loaded cumulatively.
|
|
// otherwise the default rules inherited from older ports apply.
|
|
if (userConfig.UserDef.IsNotEmpty())
|
|
{
|
|
loaddefinitionsfile(info, userConfig.UserDef.GetChars(), false);
|
|
}
|
|
else
|
|
{
|
|
if (fileSystem.FileExists(razedefsfile.GetChars()))
|
|
{
|
|
loaddefinitionsfile(info, razedefsfile.GetChars(), true);
|
|
}
|
|
else if (fileSystem.FileExists(defsfile))
|
|
{
|
|
loaddefinitionsfile(info, defsfile, false);
|
|
}
|
|
}
|
|
|
|
if (userConfig.AddDefs)
|
|
{
|
|
for (auto& m : *userConfig.AddDefs)
|
|
{
|
|
loaddefinitionsfile(info, m.GetChars(), false);
|
|
}
|
|
userConfig.AddDefs.reset();
|
|
}
|
|
|
|
if (GameStartupInfo.def.IsNotEmpty())
|
|
{
|
|
loaddefinitionsfile(info, GameStartupInfo.def.GetChars()); // Stuff from gameinfo.
|
|
}
|
|
|
|
// load the widescreen replacements last. This ensures that mods still get the correct CRCs for their own tile replacements.
|
|
if (fileSystem.FindFile("engine/widescreen.def") >= 0 && !Args->CheckParm("-nowidescreen"))
|
|
{
|
|
loaddefinitionsfile(info, "engine/widescreen.def");
|
|
}
|
|
fileSystem.InitHashChains(); // make sure that any resources that got added can be found again.
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//
|
|
//==========================================================================
|
|
|
|
picanm_t tileConvertAnimFormat(int32_t const picanimraw)
|
|
{
|
|
// Unpack a 4 byte packed anim descriptor into something more accessible
|
|
picanm_t anm;
|
|
anm.num = picanimraw & 63;
|
|
anm.sf = ((picanimraw >> 24) & 15) | (picanimraw & 192);
|
|
anm.extra = (picanimraw >> 28) & 15;
|
|
return anm;
|
|
}
|
|
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//
|
|
//==========================================================================
|
|
FImageSource* createWritableTile(int width, int height);
|
|
FImageSource* makeTileWritable(FImageSource* img);
|
|
|
|
void TilesetBuildInfo::MakeWritable(int tileno)
|
|
{
|
|
if (tile[tileno].tileimage != nullptr)
|
|
{
|
|
auto newtex = makeTileWritable(tile[tileno].tileimage);
|
|
tile[tileno].tileimage = newtex;
|
|
tile[tileno].imported = nullptr;
|
|
}
|
|
}
|
|
|
|
void TilesetBuildInfo::CreateWritable(int tileno, int w, int h)
|
|
{
|
|
auto newtex = createWritableTile(w, h);
|
|
tile[tileno].tileimage = newtex;
|
|
tile[tileno].imported = nullptr;
|
|
}
|
|
|
|
//===========================================================================
|
|
//
|
|
// MakeCanvas
|
|
//
|
|
// Turns texture into a canvas (i.e. camera texture)
|
|
//
|
|
//===========================================================================
|
|
|
|
void TilesetBuildInfo::MakeCanvas(int tilenum, int width, int height)
|
|
{
|
|
auto ftex = new FCanvasTexture(width * 4, height * 4);
|
|
ftex->aspectRatio = (float)width / height;
|
|
auto canvas = MakeGameTexture(ftex, FStringf("#%05d", tilenum).GetChars(), ETextureType::Any);
|
|
canvas->SetSize(width * 4, height * 4);
|
|
canvas->SetDisplaySize((float)width, (float)height);
|
|
canvas->GetTexture()->SetSize(width * 4, height * 4);
|
|
tile[tilenum].imported = canvas;
|
|
tile[tilenum].tileimage = nullptr;
|
|
}
|
|
|
|
static void GenerateRotations(int firsttileid, const char* basename, int tile, int numframes, int numrotations, int order)
|
|
{
|
|
if (order == 0)
|
|
{
|
|
for (int frame = 0; frame < numframes; frame++)
|
|
{
|
|
for (int rotation = 0; rotation < numrotations; rotation++)
|
|
{
|
|
FStringf str("%s@%c%x", basename, frame + 'A', rotation + 1);
|
|
TexMan.AddAlias(str.GetChars(), FSetTextureID(firsttileid + tile));
|
|
tile++;
|
|
}
|
|
}
|
|
}
|
|
else if (order >= 1)
|
|
{
|
|
for (int rotation = 0; rotation < numrotations; rotation++)
|
|
{
|
|
for (int frame = 0; frame < numframes; frame++)
|
|
{
|
|
FStringf str("%s@%c%x", basename, frame + 'A', rotation + 1);
|
|
TexMan.AddAlias(str.GetChars(), FSetTextureID(firsttileid + tile));
|
|
tile += order;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void CompleteRotations(int firsttileid, const char* basename, const char* getname, int numframes, int numrotations)
|
|
{
|
|
for (int rotation = numrotations; ; rotation++)
|
|
{
|
|
for (int frame = 0; frame < numframes; frame++)
|
|
{
|
|
FStringf str("%s@%c%x", getname, frame + 'A', rotation + 1);
|
|
auto texid = TexMan.CheckForTexture(str.GetChars(), ETextureType::Any);
|
|
if (frame == 0 && !texid.isValid())
|
|
{
|
|
// rotation does not exist for the first frame -> we reached the end.
|
|
return;
|
|
}
|
|
str.Format("%s@%c%x", basename, frame + 'A', rotation + 1);
|
|
TexMan.AddAlias(str.GetChars(), texid);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void SubstituteRotations(int firsttileid, const char* basename, int numframes, int destrot, int srcrot)
|
|
{
|
|
for (int frame = 0; frame < numframes; frame++)
|
|
{
|
|
FStringf str("%s@%c%x", basename, frame + 'A', srcrot);
|
|
auto texid = TexMan.CheckForTexture(str.GetChars(), ETextureType::Any);
|
|
if (!texid.isValid())
|
|
{
|
|
continue;
|
|
}
|
|
str.Format("%s@%c%x", basename, frame + 'A', destrot);
|
|
TexMan.AddAlias(str.GetChars(), texid);
|
|
}
|
|
}
|
|
|
|
void LoadAliases(int firsttileid, int maxarttile)
|
|
{
|
|
int lump, lastlump = 0;
|
|
while ((lump = fileSystem.FindLump("TEXNAMES", &lastlump, false)) != -1)
|
|
{
|
|
FScanner sc;
|
|
sc.OpenLumpNum(lump);
|
|
sc.SetCMode(true);
|
|
while (sc.GetNumber())
|
|
{
|
|
int tile = sc.Number;
|
|
if (tile < 0 || tile > maxarttile) tile = maxarttile;
|
|
sc.MustGetStringName("=");
|
|
sc.MustGetString();
|
|
TexMan.AddAlias(sc.String, FSetTextureID(firsttileid + tile));
|
|
FString basename = sc.String;
|
|
if (sc.CheckString(","))
|
|
{
|
|
sc.MustGetNumber();
|
|
int numframes = sc.Number;
|
|
int numrotations = 1, order = 0;
|
|
if (sc.CheckString(","))
|
|
{
|
|
sc.MustGetNumber();
|
|
numrotations = sc.Number;
|
|
if (sc.CheckString(","))
|
|
{
|
|
sc.MustGetNumber();
|
|
order = sc.Number;
|
|
}
|
|
}
|
|
if (numframes <= 0 || numframes > 26)
|
|
{
|
|
sc.ScriptMessage("%d: Bad number of frames\n", numframes);
|
|
continue;
|
|
}
|
|
if (numrotations >= 16 || numrotations < 1)
|
|
{
|
|
sc.ScriptMessage("%d: Bad number of rotations\n", numrotations);
|
|
continue;
|
|
}
|
|
if (order < 0)
|
|
{
|
|
sc.ScriptMessage("%d: Bad order\n", order);
|
|
continue;
|
|
}
|
|
GenerateRotations(firsttileid, basename.GetChars(), tile, numframes, numrotations, order);
|
|
if (sc.CheckString(","))
|
|
{
|
|
sc.MustGetString();
|
|
if (sc.String[0] != '@')
|
|
{
|
|
CompleteRotations(firsttileid, basename.GetChars(), sc.String, numframes, numrotations);
|
|
}
|
|
else
|
|
{
|
|
sc.UnGet();
|
|
do
|
|
{
|
|
sc.MustGetString();
|
|
int destrot = (int)strtoll(sc.String + 1, nullptr, 10);
|
|
sc.MustGetStringName("=");
|
|
sc.MustGetString();
|
|
int srcrot = (int)strtoll(sc.String + 1, nullptr, 10);
|
|
SubstituteRotations(firsttileid, basename.GetChars(), numframes, destrot, srcrot);
|
|
} while (sc.CheckString(","));
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
}
|
|
|
|
}
|
|
//==========================================================================
|
|
//
|
|
//
|
|
//
|
|
//==========================================================================
|
|
|
|
void ConstructTileset()
|
|
{
|
|
TilesetBuildInfo info {};
|
|
TArray<FImageSource*> images;
|
|
TArray<unsigned> rawpicanm;
|
|
GetArtImages(images, rawpicanm);
|
|
info.tile.Resize(MAXTILES);
|
|
memset(info.tile.Data(), 0, info.tile.Size() * sizeof(info.tile[0]));
|
|
// fill up the arrays to the maximum allowed but remember the highest original number.
|
|
for (unsigned i = 0; i < images.Size(); i++)
|
|
{
|
|
info.tile[i].orgimage = info.tile[i].tileimage = images[i];
|
|
if (images[i])
|
|
{
|
|
auto s = images[i]->GetOffsets();
|
|
info.tile[i].leftOffset = s.first;
|
|
info.tile[i].topOffset = s.second;
|
|
}
|
|
info.tile[i].extinfo.picanm = tileConvertAnimFormat(rawpicanm[i]);
|
|
}
|
|
images.Reset();
|
|
rawpicanm.Reset();
|
|
for (auto& a : info.tile)
|
|
{
|
|
a.alphathreshold = 0.5f;
|
|
a.extinfo.tiletovox = -1;
|
|
}
|
|
gi->LoadTextureInfo(info); // initialize game data that must be done before loading .DEF
|
|
LoadDefinitions(info);
|
|
gi->SetupSpecialTextures(info); // initialize game data that needs .DEF being processed.
|
|
|
|
// now that everything has been set up, we can add the textures to the texture manager.
|
|
// To keep things simple everything from .ART files and its replacements will remain in order and
|
|
// converting between a Build tilenum and a texture ID can done with a single addition.
|
|
// Even though this requires adding quite a few empty textures to the texture manager, it makes things a lot easier,
|
|
// because it ensures an unambiguous mapping and allows communicating with features that only work with tile numbers.
|
|
// as long as no named textures are used.
|
|
|
|
auto nulltex = TexMan.GameByIndex(0); // Use the null texture's backing data for all empty placeholders.
|
|
// Only the outward facing FGameTexture needs to be different
|
|
|
|
firstarttile = TexMan.NumTextures();
|
|
maxarttile = MAXTILES - 1;
|
|
while (maxarttile >= 0 && info.tile[maxarttile].tileimage == nullptr) maxarttile--;
|
|
if (maxarttile < 0) return; // should never happen, but who knows - maybe someone will make a game without ART files later... :D
|
|
maxarttile++; // create a placeholder in the first unused spot. This will later get used for all out of range tile numbers.
|
|
|
|
int lastid = firstarttile - 1;
|
|
for (int i = 0; i <= maxarttile; i++)
|
|
{
|
|
FTexture* ftex = nullptr;
|
|
FGameTexture* gtex;
|
|
FStringf tname("#%05d", i);
|
|
if (info.tile[i].tileimage == nullptr)
|
|
{
|
|
if (info.tile[i].imported == nullptr || i == 0)
|
|
{
|
|
ftex = nulltex->GetTexture();
|
|
gtex = MakeGameTexture(ftex, tname.GetChars(), ETextureType::Null);
|
|
}
|
|
else
|
|
{
|
|
// Canvas textures can be used directly without wrapping them again.
|
|
gtex = info.tile[i].imported;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (info.tile[i].imported) ftex = info.tile[i].imported->GetTexture();
|
|
else ftex = new FImageTexture(info.tile[i].tileimage);
|
|
gtex = MakeGameTexture(ftex, tname.GetChars(), i == 0? ETextureType::FirstDefined : ETextureType::Any);
|
|
gtex->SetOffsets(info.tile[i].leftOffset, info.tile[i].topOffset);
|
|
}
|
|
if (info.tile[i].extinfo.picanm.sf & PICANM_NOFULLBRIGHT_BIT)
|
|
{
|
|
gtex->SetDisableFullbright(true);
|
|
}
|
|
|
|
auto id = TexMan.AddGameTexture(gtex, true);
|
|
if (id.GetIndex() != lastid + 1)
|
|
{
|
|
// this should never happen unless the texture manager gets redone in an incompatible fashion.
|
|
I_FatalError("Unable to assign consecutive texture IDs to tile set.");
|
|
}
|
|
lastid = id.GetIndex();
|
|
}
|
|
// Now create the extended info. This will leave room for all regular textures as well so that later code can assign info to these, too.
|
|
// Textures being added afterward will always see the default extinfo, even if they are not covered by this array.
|
|
texExtInfo.Resize(TexMan.NumTextures());
|
|
memset(texExtInfo.Data(), 0, sizeof(texExtInfo[0]) * texExtInfo.Size());
|
|
for (auto& x : texExtInfo) x.tiletovox = -1;
|
|
// now copy all extinfo stuff that got parsed by .DEF or some game specific setup.
|
|
for (int i = 0; i <= maxarttile; i++)
|
|
{
|
|
texExtInfo[i + firstarttile] = info.tile[i].extinfo;
|
|
}
|
|
|
|
LoadAliases(firstarttile, maxarttile);
|
|
|
|
for (auto& a : info.aliases)
|
|
{
|
|
TexMan.AddAlias(a.first.GetChars(), min(maxarttile, a.second) + firstarttile);
|
|
}
|
|
}
|