VVVVVV/desktop_version/src/CustomLevels.cpp

1976 lines
51 KiB
C++
Raw Normal View History

#define CL_DEFINITION
#include "CustomLevels.h"
#include <physfs.h>
#include <stdio.h>
#include <string>
#include <tinyxml2.h>
#include "Alloc.h"
#include "Constants.h"
#include "Editor.h"
#include "Enums.h"
#include "FileSystemUtils.h"
Start rewrite of font system This is still a work in progress, but the existing font system has been removed and replaced by a new one, in Font.cpp. Design goals of the new font system include supporting colored button glyphs, different fonts for different languages, and larger fonts than 8x8 for Chinese, Japanese and Korean, while being able to support their 30000+ characters without hiccups, slowdowns or high memory usage. And to have more flexibility with fonts in general. Plus, Graphics.cpp was long enough as-is, so it's good to have a dedicated file for font storage. The old font system worked with a std::vector<SDL_Surface*> to store 8x8 surfaces for each character, and a std::map<int,int> to store mappings between codepoints and vector indexes. The new system has a per-font collection of pages for every block of 0x1000 (4096) codepoints, that may be allocated as needed. A glyph on a page contains the index of the glyph in the image (giving its coordinates), the advance (how much the cursor should advance, so the width of that glyph) and some flags which would be at least whether the glyph exists and whether it is colored. Most of the *new* features aren't implemented yet; it's currently hardcoded to the regular 8x8 font.png, but it should be functionally equivalent to the previous behavior. The only thing that doesn't really work yet is level-specific font.png, but that'll be supported again soon enough. This commit also adds fontmeta (xml) support. Since the fonts folder is mounted at graphics/, there are two main options for recognizing non-font.png fonts: the font files have to be prefixed with font (or font_) or some special file extension is involved to signal what files are fonts. I always had a font.xml in mind (so font_cn.xml, font_ja.xml, etc) but if there's ever gonna be a need for further xml files inside the graphics folder, we have a problem. So I named them .fontmeta instead. A .fontmeta file looks somewhat like this: <?xml version="1.0" encoding="UTF-8"?> <font_metadata> <width>12</width> <height>12</height> <white_teeth>1</white_teeth> <chars> <range start="0x20" end="0x7E"/> <range start="0x80" end="0x80"/> <range start="0xA0" end="0xDF"/> <range start="0x250" end="0x2A8"/> <range start="0x2AD" end="0x2AD"/> <range start="0x2C7" end="0x2C7"/> <range start="0x2C9" end="0x2CB"/> ... </chars> <special> <range start="0x00" end="0x1F" advance="6"/> <range start="0x61" end="0x66" color="1"/> <range start="0x63" end="0x63" color="0"/> </special> </font_metadata> The <chars> tag can be used to specify characters instead of in a .txt. The original idea was to just always use the existing .txt system for specifying the font charset, and only use the XML for the other stuff that the .txt doesn't cover. However, it's probably better to keep it simple if possible - having to only have a .png and a .fontmeta seems simpler than having the data spread out over three files. And a major advantage: Chinese fonts can have about 30000 characters! It's more efficient to be able to have a tag saying "now there's 20902 characters starting at U+4E00" than to include them all in a text file and having to UTF-8 decode every single one of them. If a font.txt exists, it takes priority over the <chars> tag, and in that case, there's no reason to include the <chars> tag in the XML. But font.txt has to be in the same directory as font.png, otherwise it is rejected. Same for font.fontmeta. If neither font.txt nor <chars> exist, then the font is seen as a 2.2-and-below-style ASCII font. In <special>: advance is the number of pixels the cursor advances after drawing the character (so the width of the character, without affecting the grid in the source image), color is whether the character should have its original colors retained when printed (for button glyphs). As for <white_teeth>: The renderer PR has replaced draw-time whitening of sprites/etc (using BlitSurfaceColoured) by load-time whitening of entire images (using LoadImage with TEX_WHITE as an argument). This means we have a problem: fonts have always had their glyphs whitened at printing time, and since I'm adding support for colored button glyphs, I changed it so glyphs would sometimes not be whitened. But if we can't whiten at print time, then we'd need to whiten at load time, and if we whiten the entire font, any colored glyphs will get destroyed too. If you whiten the image selectively, well, we need more code to target specific squares in the image, and it's kind of a waste when you need to whiten 30000 12x12 Chinese characters when you're only going to need a handful, but you don't know which ones. The solution: Whitening fonts is useless if all the non-colored glyphs are already white, so we don't need to do it anyway! However, any existing fonts that have non-white glyphs (and I know of at least one level like that) will still need to be whitened. So there is now a font property <white_teeth> that can be specified in the fontmeta, which indicates that the font is already pre-whitened. If not specified, traditional whitening behavior will be used, and the font cannot use colored glyphs.
2023-01-02 05:14:53 +01:00
#include "Font.h"
#include "Game.h"
#include "Graphics.h"
#include "GraphicsUtil.h"
#include "KeyPoll.h"
#include "Localization.h"
#include "LocalizationStorage.h"
#include "Map.h"
#include "Screen.h"
#include "Script.h"
#include "UTF8.h"
#include "UtilityClass.h"
#include "Vlogging.h"
#include "XMLUtils.h"
#ifdef _WIN32
#define SCNx32 "x"
#define SCNu32 "u"
#else
#ifndef __STDC_FORMAT_MACROS
#define __STDC_FORMAT_MACROS
#endif
#ifndef _POSIX_SOURCE
#define _POSIX_SOURCE
#endif
#include <inttypes.h>
#endif
#define VMULT(y) (y * SCREEN_WIDTH_TILES * maxwidth)
#define Y_INBOUNDS(y) (y >= 0 && y < SCREEN_HEIGHT_TILES * maxheight)
RoomProperty::RoomProperty(void)
{
tileset=0;
tilecol=0;
warpdir=0;
platx1=0;
platy1=0;
platx2=320;
platy2=240;
platv=4;
enemyx1=0;
enemyy1=0;
enemyx2=320;
enemyy2=240;
enemytype=0;
directmode=0;
}
customlevelclass::customlevelclass(void)
{
reset();
}
// comparison, not case sensitive.
static bool compare_nocase (std::string first, std::string second)
{
unsigned int i=0;
while ( (i<first.length()) && (i<second.length()) )
{
if (SDL_tolower(first[i])<SDL_tolower(second[i]))
return true;
else if (SDL_tolower(first[i])>SDL_tolower(second[i]))
return false;
++i;
}
if (first.length()<second.length())
return true;
else
return false;
}
/* translate_title and translate_creator are used to display default title/author
* as being translated, while they're actually stored in English in the level file.
* This way we translate "Untitled Level" and "Unknown" without
* spreading around translations in level files posted online! */
bool translate_title(const std::string& title)
{
return title == "Untitled Level";
}
bool translate_creator(const std::string& creator)
{
return creator == "Unknown";
}
static void levelZipCallback(const char* filename)
{
if (!FILESYSTEM_isFile(filename))
{
return;
}
if (endsWith(filename, ".zip"))
{
FILESYSTEM_loadZip(filename);
}
}
void customlevelclass::loadZips(void)
{
FILESYSTEM_enumerateLevelDirFileNames(levelZipCallback);
}
static void replace_all(std::string& str, const std::string& from, const std::string& to)
{
if (from.empty())
{
return;
}
size_t start_pos = 0;
while ((start_pos = str.find(from, start_pos)) != std::string::npos)
{
str.replace(start_pos, from.length(), to);
start_pos += to.length(); //In case `to` contains `from`, like replacing 'x' with 'yx'
}
}
static std::string find_tag(const std::string& buf, const std::string& start, const std::string& end)
{
size_t tag = buf.find(start);
if (tag == std::string::npos)
{
//No start tag
return "";
}
size_t tag_start = tag + start.size();
size_t tag_close = buf.find(end, tag_start);
if (tag_close == std::string::npos)
{
//No close tag
return "";
}
size_t tag_len = tag_close - tag_start;
std::string value(buf.substr(tag_start, tag_len));
//Encode special XML entities
replace_all(value, "&quot;", "\"");
replace_all(value, "&amp;", "&");
replace_all(value, "&apos;", "'");
replace_all(value, "&lt;", "<");
replace_all(value, "&gt;", ">");
//Encode general XML entities
size_t start_pos = 0;
while ((start_pos = value.find("&#", start_pos)) != std::string::npos)
{
if (start_pos + 2 >= value.length())
{
return "";
}
bool hex = value[start_pos + 2] == 'x';
size_t end = value.find(';', start_pos);
if (end == std::string::npos)
{
return "";
}
size_t real_start = start_pos + 2 + ((int) hex);
std::string number(value.substr(real_start, end - real_start));
if (!is_positive_num(number.c_str(), hex))
{
return "";
}
uint32_t character = 0;
if (hex)
{
SDL_sscanf(number.c_str(), "%" SCNx32, &character);
}
else
{
SDL_sscanf(number.c_str(), "%" SCNu32, &character);
}
value.replace(start_pos, end - start_pos + 1, UTF8_encode(character).bytes);
}
return value;
}
#define TAG_FINDER(NAME, TAG) \
static std::string NAME(const std::string& buf) \
{ \
return find_tag(buf, "<" TAG ">", "</" TAG ">"); \
}
TAG_FINDER(find_metadata, "MetaData") //only for checking that it exists
TAG_FINDER(find_creator, "Creator")
TAG_FINDER(find_title, "Title")
TAG_FINDER(find_desc1, "Desc1")
TAG_FINDER(find_desc2, "Desc2")
TAG_FINDER(find_desc3, "Desc3")
TAG_FINDER(find_website, "website")
TAG_FINDER(find_font, "font")
TAG_FINDER(find_rtl, "rtl")
/* For CliPlaytestArgs */
TAG_FINDER(find_playtest, "Playtest")
TAG_FINDER(find_playx, "playx")
TAG_FINDER(find_playy, "playy")
TAG_FINDER(find_playrx, "playrx")
TAG_FINDER(find_playry, "playry")
TAG_FINDER(find_playgc, "playgc")
TAG_FINDER(find_playmusic, "playmusic")
#undef TAG_FINDER
static void levelMetaDataCallback(const char* filename)
{
extern customlevelclass cl;
LevelMetaData temp;
std::string filename_ = filename;
if (!endsWith(filename, ".vvvvvv")
|| !FILESYSTEM_isFile(filename)
|| FILESYSTEM_isMounted(filename))
{
return;
}
if (cl.getLevelMetaData(filename_, temp))
{
cl.ListOfMetaData.push_back(temp);
}
else
{
vlog_warn("Level %s not found :(", filename_.c_str());
}
}
void customlevelclass::getDirectoryData(void)
{
ListOfMetaData.clear();
FILESYSTEM_clearLevelDirError();
loadZips();
FILESYSTEM_enumerateLevelDirFileNames(levelMetaDataCallback);
for(size_t i = 0; i < ListOfMetaData.size(); i++)
{
for(size_t k = 0; k < ListOfMetaData.size(); k++)
{
if(compare_nocase(ListOfMetaData[i].title, ListOfMetaData[k].title ))
{
std::swap(ListOfMetaData[i] , ListOfMetaData[k]);
}
}
}
}
bool customlevelclass::getLevelMetaDataAndPlaytestArgs(const std::string& _path, LevelMetaData& _data, CliPlaytestArgs* pt_args)
{
unsigned char *uMem;
FILESYSTEM_loadFileToMemory(_path.c_str(), &uMem, NULL);
if (uMem == NULL)
{
return false;
}
std::string buf((char*) uMem);
VVV_free(uMem);
if (find_metadata(buf) == "")
{
vlog_warn("Couldn't load metadata for %s", _path.c_str());
return false;
}
_data.creator = find_creator(buf);
_data.creator_is_gettext = translate_creator(_data.creator);
_data.title = find_title(buf);
_data.title_is_gettext = translate_title(_data.title);
_data.Desc1 = find_desc1(buf);
_data.Desc2 = find_desc2(buf);
_data.Desc3 = find_desc3(buf);
_data.website = find_website(buf);
if (!font::find_main_font_by_name(find_font(buf).c_str(), &_data.level_main_font_idx))
{
_data.level_main_font_idx = font::get_font_idx_8x8();
}
_data.rtl = help.Int(find_rtl(buf).c_str());
if (pt_args != NULL)
{
const std::string playtest = find_playtest(buf);
if (playtest == "")
{
pt_args->valid = false;
}
else
{
pt_args->valid = true;
pt_args->x = help.Int(find_playx(playtest).c_str());
pt_args->y = help.Int(find_playy(playtest).c_str());
pt_args->rx = help.Int(find_playrx(playtest).c_str());
pt_args->ry = help.Int(find_playry(playtest).c_str());
pt_args->gc = help.Int(find_playgc(playtest).c_str());
pt_args->music = help.Int(find_playmusic(playtest).c_str());
}
}
_data.filename = _path;
return true;
}
bool customlevelclass::getLevelMetaData(const std::string& _path, LevelMetaData& _data)
{
return getLevelMetaDataAndPlaytestArgs(_path, _data, NULL);
}
void customlevelclass::reset(void)
{
version=2; //New smaller format change is 2
mapwidth=5;
mapheight=5;
title="Untitled Level"; // Already translatable
creator="Unknown";
levmusic=0;
customentities.clear();
levmusic=0;
for (int j = 0; j < maxheight; j++)
{
for (int i = 0; i < maxwidth; i++)
{
roomproperties[i+(j*maxwidth)].tileset=0;
roomproperties[i+(j*maxwidth)].tilecol=(i+j)%32;
roomproperties[i+(j*maxwidth)].roomname="";
roomproperties[i+(j*maxwidth)].warpdir=0;
roomproperties[i+(j*maxwidth)].platx1=0;
roomproperties[i+(j*maxwidth)].platy1=0;
roomproperties[i+(j*maxwidth)].platx2=320;
roomproperties[i+(j*maxwidth)].platy2=240;
roomproperties[i+(j*maxwidth)].platv=4;
roomproperties[i+(j*maxwidth)].enemyx1=0;
roomproperties[i+(j*maxwidth)].enemyy1=0;
roomproperties[i+(j*maxwidth)].enemyx2=320;
roomproperties[i+(j*maxwidth)].enemyy2=240;
roomproperties[i+(j*maxwidth)].enemytype=0;
roomproperties[i+(j*maxwidth)].directmode=0;
}
}
SDL_zeroa(contents);
script.clearcustom();
onewaycol_override = false;
script.textbox_colours.clear();
script.add_default_colours();
map.specialroomnames.clear();
}
const int* customlevelclass::loadlevel( int rxi, int ryi )
{
//Set up our buffer array to be picked up by mapclass
rxi -= 100;
ryi -= 100;
if(rxi<0)rxi+=mapwidth;
if(ryi<0)ryi+=mapheight;
if(rxi>=mapwidth)rxi-=mapwidth;
if(ryi>=mapheight)ryi-=mapheight;
static int result[1200];
for (int j = 0; j < 30; j++)
{
for (int i = 0; i < 40; i++)
{
result[i + j*40] = gettile(rxi, ryi, i, j);
}
}
return result;
}
int customlevelclass::getlevelcol(const int tileset, const int tilecol)
{
if(tileset==0) //Space Station
{
return tilecol;
}
else if(tileset==1) //Outside
{
return 32+tilecol;
}
else if(tileset==2) //Lab
{
return 40+tilecol;
}
else if(tileset==3) //Warp Zone
{
return 46+tilecol;
}
else if(tileset==4) //Ship
{
return 52+tilecol;
}
return 0;
}
int customlevelclass::getenemycol(int t)
{
switch(t)
{
//RED
case 3:
case 7:
case 12:
case 23:
case 28:
case 34:
case 42:
case 48:
case 58:
return 6;
break;
//GREEN
case 5:
case 9:
case 22:
case 25:
case 29:
case 31:
case 38:
case 46:
case 52:
case 53:
return 7;
break;
//BLUE
case 1:
case 6:
case 14:
case 27:
case 33:
case 44:
case 50:
case 57:
return 12;
break;
//YELLOW
case 4:
case 17:
case 24:
case 30:
case 37:
case 45:
case 51:
case 55:
return 9;
break;
//PURPLE
case 2:
case 11:
case 15:
case 19:
case 32:
case 36:
case 49:
return 20;
break;
//CYAN
case 8:
case 10:
case 13:
case 18:
case 26:
case 35:
case 41:
case 47:
case 54:
return 11;
break;
//PINK
case 16:
case 20:
case 39:
case 43:
case 56:
return 8;
break;
//ORANGE
case 21:
case 40:
return 17;
break;
default:
return 6;
break;
}
}
int customlevelclass::getwarpbackground(int rx, int ry)
{
const RoomProperty* const room = getroomprop(rx, ry);
switch(room->tileset)
{
case 0: //Space Station
switch(room->tilecol)
{
case 0:
return 3;
break;
case 1:
return 2;
break;
case 2:
return 1;
break;
case 3:
return 4;
break;
case 4:
return 5;
break;
case 5:
return 3;
break;
case 6:
return 1;
break;
case 7:
return 0;
break;
case 8:
return 5;
break;
case 9:
return 0;
break;
case 10:
return 2;
break;
case 11:
return 1;
break;
case 12:
return 5;
break;
case 13:
return 0;
break;
case 14:
return 3;
break;
case 15:
return 2;
break;
case 16:
return 4;
break;
case 17:
return 0;
break;
case 18:
return 3;
break;
case 19:
return 1;
break;
case 20:
return 4;
break;
case 21:
return 5;
break;
case 22:
return 1;
break;
case 23:
return 4;
break;
case 24:
return 5;
break;
case 25:
return 0;
break;
case 26:
return 3;
break;
case 27:
return 1;
break;
case 28:
return 5;
break;
case 29:
return 4;
break;
case 30:
return 5;
break;
case 31:
return 2;
break;
default:
return 6;
break;
}
break;
case 1: //Outside
switch(room->tilecol)
{
case 0:
return 3;
break;
case 1:
return 1;
break;
case 2:
return 0;
break;
case 3:
return 2;
break;
case 4:
return 4;
break;
case 5:
return 5;
break;
case 6:
return 2;
break;
case 7:
return 4;
break;
default:
return 6;
break;
}
break;
case 2: //Lab
switch(room->tilecol)
{
case 0:
return 0;
break;
case 1:
return 1;
break;
case 2:
return 2;
break;
case 3:
return 3;
break;
case 4:
return 4;
break;
case 5:
return 5;
break;
case 6:
return 6;
break;
default:
return 6;
break;
}
break;
case 3: //Warp Zone
switch(room->tilecol)
{
case 0:
return 0;
break;
case 1:
return 1;
break;
case 2:
return 2;
break;
case 3:
return 3;
break;
case 4:
return 4;
break;
case 5:
return 5;
break;
case 6:
return 6;
break;
default:
return 6;
break;
}
break;
case 4: //Ship
switch(room->tilecol)
{
case 0:
return 5;
break;
case 1:
return 0;
break;
case 2:
return 4;
break;
case 3:
return 2;
break;
case 4:
return 3;
break;
case 5:
return 1;
break;
case 6:
return 6;
break;
default:
return 6;
break;
}
break;
case 5: //Tower
return 6;
break;
default:
return 6;
break;
}
}
int customlevelclass::gettileidx(
const int rx,
const int ry,
const int x,
const int y
) {
const int yoff = y + ry*30;
int mult;
int idx;
if (Y_INBOUNDS(yoff))
{
mult = VMULT(yoff);
}
else
{
mult = 0;
}
idx = x + rx*40 + mult;
return idx;
}
void customlevelclass::settile(
const int rx,
const int ry,
const int x,
const int y,
const int t
) {
const int idx = gettileidx(rx, ry, x, y);
if (!INBOUNDS_ARR(idx, contents))
{
return;
}
contents[idx] = t;
}
int customlevelclass::gettile(
const int rx,
const int ry,
const int x,
const int y
) {
const int idx = gettileidx(rx, ry, x, y);
if (!INBOUNDS_ARR(idx, contents))
{
return 0;
}
return contents[idx];
}
int customlevelclass::getabstile(const int x, const int y)
{
int idx;
int yoff;
if (Y_INBOUNDS(y))
{
yoff = VMULT(y);
}
else
{
yoff = 0;
}
idx = x + yoff;
if (!INBOUNDS_ARR(idx, contents))
{
return 0;
}
return contents[idx];
}
int customlevelclass::getroompropidx(const int rx, const int ry)
{
return rx + ry*maxwidth;
}
const RoomProperty* customlevelclass::getroomprop(const int rx, const int ry)
{
const int idx = getroompropidx(rx, ry);
if (INBOUNDS_ARR(idx, roomproperties))
{
return &roomproperties[idx];
}
static RoomProperty blank;
blank.tileset = 1;
blank.directmode = 1;
blank.roomname.clear();
return &blank;
}
#define FOREACH_PROP(NAME, TYPE) \
void customlevelclass::setroom##NAME(const int rx, const int ry, const TYPE NAME) \
{ \
const int idx = getroompropidx(rx, ry); \
\
if (!INBOUNDS_ARR(idx, roomproperties)) \
{ \
return; \
} \
\
roomproperties[idx].NAME = NAME; \
}
ROOM_PROPERTIES
#undef FOREACH_PROP
int customlevelclass::absfree( int x, int y )
{
//Returns 0 if tile is not a block, 1 otherwise, abs on grid
if(x>=0 && y>=0 && x<mapwidth*40 && y<mapheight*30)
{
if(getabstile(x, y)==0)
{
return 0;
}
else
{
if(getabstile(x, y)>=2 && getabstile(x, y)<80)
{
return 0;
}
if(getabstile(x, y)>=680)
{
return 0;
}
}
}
return 1;
}
void customlevelclass::findstartpoint(void)
{
//Ok! Scan the room for the closest checkpoint
int testeditor=-1;
//First up; is there a start point on this screen?
for(size_t i=0; i<customentities.size(); i++)
{
//if() on screen
if(customentities[i].t==16 && testeditor==-1)
{
testeditor=i;
}
}
if(testeditor==-1)
{
game.edsavex = 160;
game.edsavey = 120;
game.edsaverx = 100;
game.edsavery = 100;
game.edsavegc = 0;
game.edsavey--;
game.edsavedir=1;
}
else
{
//Start point spawn
game.edsavex = (customentities[testeditor].x * 8) - 4;
game.edsavey = customentities[testeditor].y * 8;
game.edsaverx = 100 + customentities[testeditor].rx;
game.edsavery = 100 + customentities[testeditor].ry;
game.edsavegc = 0;
game.edsavey++;
game.edsavedir=1-customentities[testeditor].p1;
}
}
int customlevelclass::findtrinket(int t)
{
int ttrinket=0;
for(int i=0; i<(int)customentities.size(); i++)
{
if(i==t) return ttrinket;
if(customentities[i].t==9) ttrinket++;
}
return 0;
}
int customlevelclass::findcrewmate(int t)
{
int ttrinket=0;
for(int i=0; i<(int)customentities.size(); i++)
{
if(i==t) return ttrinket;
if(customentities[i].t==15) ttrinket++;
}
return 0;
}
int customlevelclass::findwarptoken(int t)
{
int ttrinket=0;
for(int i=0; i<(int)customentities.size(); i++)
{
if(i==t) return ttrinket;
if(customentities[i].t==13) ttrinket++;
}
return 0;
}
bool customlevelclass::load(std::string _path)
{
tinyxml2::XMLDocument doc;
tinyxml2::XMLHandle hDoc(&doc);
tinyxml2::XMLElement* pElem;
reset();
ed.reset();
static const char *levelDir = "levels/";
if (_path.compare(0, SDL_strlen(levelDir), levelDir) != 0)
{
_path = levelDir + _path;
}
FILESYSTEM_unmountAssets();
if (game.cliplaytest && game.playassets != "")
{
MAYBE_FAIL(FILESYSTEM_mountAssets(game.playassets.c_str()));
}
else
{
MAYBE_FAIL(FILESYSTEM_mountAssets(_path.c_str()));
}
if (!FILESYSTEM_loadTiXml2Document(_path.c_str(), doc))
{
FILESYSTEM_setLevelDirError(
loc::gettext("Level {path} not found"),
"path:str",
_path.c_str()
);
goto fail;
}
if (doc.Error())
{
FILESYSTEM_setLevelDirError(
loc::gettext("Error parsing {path}: {error}"),
"path:str, error:str",
_path.c_str(),
doc.ErrorStr()
);
goto fail;
}
ed.loaded_filepath = _path;
version = 0;
level_font_name = "font";
rtl = false;
for (pElem = hDoc
.FirstChildElement()
.FirstChildElement("Data")
.FirstChildElement()
.ToElement();
pElem != NULL;
pElem = pElem->NextSiblingElement())
{
const char* pKey = pElem->Value();
const char* pText = pElem->GetText();
if(pText == NULL)
{
pText = "";
}
if (SDL_strcmp(pKey, "MetaData") == 0)
{
for( tinyxml2::XMLElement* subElem = pElem->FirstChildElement(); subElem; subElem= subElem->NextSiblingElement())
{
const char* pKey_ = subElem->Value();
const char* pText_ = subElem->GetText() ;
if(pText_ == NULL)
{
pText_ = "";
}
if(SDL_strcmp(pKey_, "Creator") == 0)
{
creator = pText_;
}
if(SDL_strcmp(pKey_, "Title") == 0)
{
title = pText_;
}
if(SDL_strcmp(pKey_, "Desc1") == 0)
{
Desc1 = pText_;
}
if(SDL_strcmp(pKey_, "Desc2") == 0)
{
Desc2 = pText_;
}
if(SDL_strcmp(pKey_, "Desc3") == 0)
{
Desc3 = pText_;
}
if(SDL_strcmp(pKey_, "website") == 0)
{
website = pText_;
}
if(SDL_strcmp(pKey_, "onewaycol_override") == 0)
{
onewaycol_override = help.Int(pText_);
}
if(SDL_strcmp(pKey_, "font") == 0)
{
level_font_name = pText_;
}
if(SDL_strcmp(pKey_, "rtl") == 0)
{
rtl = help.Int(pText_);
}
}
}
if (SDL_strcmp(pKey, "mapwidth") == 0)
{
mapwidth = help.Int(pText);
}
if (SDL_strcmp(pKey, "mapheight") == 0)
{
mapheight = help.Int(pText);
}
if (SDL_strcmp(pKey, "levmusic") == 0)
{
levmusic = help.Int(pText);
}
if (SDL_strcmp(pKey, "contents") == 0 && pText[0] != '\0')
{
int x = 0;
int y = 0;
char buffer[16];
size_t start = 0;
while (next_split_s(buffer, sizeof(buffer), &start, pText, ','))
{
const int idx = x + maxwidth*40*y;
if (INBOUNDS_ARR(idx, contents))
{
contents[idx] = help.Int(buffer);
}
++x;
if (x == mapwidth*40)
{
x = 0;
++y;
}
}
}
if (SDL_strcmp(pKey, "edEntities") == 0)
{
for( tinyxml2::XMLElement* edEntityEl = pElem->FirstChildElement(); edEntityEl; edEntityEl=edEntityEl->NextSiblingElement())
{
CustomEntity entity = CustomEntity();
const char* text = edEntityEl->GetText();
int global_x = 0;
int global_y = 0;
if (text != NULL)
{
size_t len = SDL_strlen(text);
// And now we come to the part where we have to deal with
// the terrible decisions of the past.
//
// For some reason, the closing tag of edentities generated
// by 2.2 and below has not only been put on a separate
// line, but also indented to match with the opening tag as
// well. Like this:
//
// <edentity ...>contents
// </edentity>
//
// Instead of doing <edentity ...>contents</edentity>.
//
// This is COMPLETELY terrible. This requires the XML to be
// parsed in an extremely specific and quirky way, which
// TinyXML-1 just happened to do.
//
// TinyXML-2 by default interprets the newline and the next
// indentation of whitespace literally, so you end up with
// tag contents that has a linefeed plus a bunch of extra
// spaces. You can't fix this by setting the whitespace
// mode to COLLAPSE_WHITESPACE, that does way more than
// TinyXML-1 ever did - it removes the leading whitespace
// from things like <edentity ...> this</edentity>, and
// collapses XML-encoded whitespace like <edentity ...>
// &#32; &#32;this</edentity>, which TinyXML-1 never did.
//
// Best solution here is to specifically hardcode removing
// the linefeed + the extremely specific amount of
// whitespace at the end of the contents.
if (endsWith(text, "\n ")) // linefeed + exactly 12 spaces
{
// 12 spaces + 1 linefeed = 13 chars
len -= 13;
}
entity.scriptname = std::string(text, len);
}
edEntityEl->QueryIntAttribute("x", &global_x);
edEntityEl->QueryIntAttribute("y", &global_y);
entity.rx = global_x / SCREEN_WIDTH_TILES;
entity.x = global_x % SCREEN_WIDTH_TILES;
entity.ry = global_y / SCREEN_HEIGHT_TILES;
entity.y = global_y % SCREEN_HEIGHT_TILES;
edEntityEl->QueryIntAttribute("t", &entity.t);
edEntityEl->QueryIntAttribute("p1", &entity.p1);
edEntityEl->QueryIntAttribute("p2", &entity.p2);
edEntityEl->QueryIntAttribute("p3", &entity.p3);
edEntityEl->QueryIntAttribute("p4", &entity.p4);
edEntityEl->QueryIntAttribute("p5", &entity.p5);
edEntityEl->QueryIntAttribute("p6", &entity.p6);
customentities.push_back(entity);
}
}
if (SDL_strcmp(pKey, "levelMetaData") == 0)
{
int i = 0;
for( tinyxml2::XMLElement* edLevelClassElement = pElem->FirstChildElement(); edLevelClassElement; edLevelClassElement=edLevelClassElement->NextSiblingElement())
{
if (!INBOUNDS_ARR(i, roomproperties))
{
continue;
}
if(edLevelClassElement->GetText() != NULL)
{
roomproperties[i].roomname = std::string(edLevelClassElement->GetText()) ;
}
edLevelClassElement->QueryIntAttribute("tileset", &roomproperties[i].tileset);
edLevelClassElement->QueryIntAttribute("tilecol", &roomproperties[i].tilecol);
edLevelClassElement->QueryIntAttribute("platx1", &roomproperties[i].platx1);
edLevelClassElement->QueryIntAttribute("platy1", &roomproperties[i].platy1);
edLevelClassElement->QueryIntAttribute("platx2", &roomproperties[i].platx2);
edLevelClassElement->QueryIntAttribute("platy2", &roomproperties[i].platy2);
edLevelClassElement->QueryIntAttribute("platv", &roomproperties[i].platv);
edLevelClassElement->QueryIntAttribute("enemyx1", &roomproperties[i].enemyx1);
edLevelClassElement->QueryIntAttribute("enemyy1", &roomproperties[i].enemyy1);
edLevelClassElement->QueryIntAttribute("enemyx2", &roomproperties[i].enemyx2);
edLevelClassElement->QueryIntAttribute("enemyy2", &roomproperties[i].enemyy2);
edLevelClassElement->QueryIntAttribute("enemytype", &roomproperties[i].enemytype);
edLevelClassElement->QueryIntAttribute("directmode", &roomproperties[i].directmode);
edLevelClassElement->QueryIntAttribute("warpdir", &roomproperties[i].warpdir);
i++;
}
}
if (SDL_strcmp(pKey, "script") == 0 && pText[0] != '\0')
{
Script script_;
bool headerfound = false;
size_t start = 0;
size_t len = 0;
size_t prev_start = 0;
while (next_split(&start, &len, &pText[start], '|'))
{
if (len > 0 && pText[prev_start + len - 1] == ':')
{
if (headerfound)
{
script.customscripts.push_back(script_);
}
script_.name = std::string(&pText[prev_start], len - 1);
script_.contents.clear();
headerfound = true;
goto next;
}
if (headerfound)
{
script_.contents.push_back(std::string(&pText[prev_start], len));
}
next:
prev_start = start;
}
/* Add the last script */
if (headerfound)
{
script.customscripts.push_back(script_);
}
}
if (SDL_strcmp(pKey, "TextboxColours") == 0)
{
for (tinyxml2::XMLElement* textColourElement = pElem->FirstChildElement(); textColourElement; textColourElement = textColourElement->NextSiblingElement())
{
if (SDL_strcmp(textColourElement->Value(), "colour") == 0)
{
int r = 255;
int g = 255;
int b = 255;
textColourElement->QueryIntAttribute("r", &r);
textColourElement->QueryIntAttribute("g", &g);
textColourElement->QueryIntAttribute("b", &b);
const char* name = textColourElement->Attribute("name");
if (name != NULL)
{
SDL_Colour colour;
colour.r = r;
colour.g = g;
colour.b = b;
script.textbox_colours[name] = colour;
}
}
}
}
if (SDL_strcmp(pKey, "SpecialRoomnames") == 0)
{
for (tinyxml2::XMLElement* roomnameElement = pElem->FirstChildElement(); roomnameElement; roomnameElement = roomnameElement->NextSiblingElement())
{
const char* roomnameType = roomnameElement->Value();
Roomname name;
name.x = 0;
name.y = 0;
name.flag = -1;
name.loop = false;
name.type = RoomnameType_STATIC;
name.progress = 0;
name.delay = 0;
if (SDL_strcmp(roomnameType, "transform") == 0)
{
name.type = RoomnameType_TRANSFORM;
name.delay = 2;
}
else if (SDL_strcmp(roomnameType, "glitch") == 0)
{
name.type = RoomnameType_GLITCH;
name.progress = 1;
name.delay = -1;
}
name.text.clear();
roomnameElement->QueryIntAttribute("x", &name.x);
roomnameElement->QueryIntAttribute("y", &name.y);
roomnameElement->QueryIntAttribute("flag", &name.flag);
roomnameElement->QueryBoolAttribute("loop", &name.loop);
// Rooms start at (100, 100) instead of (0, 0), so offset the coordinates
name.x += 100;
name.y += 100;
if (name.type == RoomnameType_STATIC)
{
const char* text = roomnameElement->GetText();
if (text != NULL)
{
name.text.push_back(std::string(text));
}
}
else
{
// Does it have children?
if (roomnameElement->FirstChildElement() == NULL)
{
continue;
}
for (tinyxml2::XMLElement* textElement = roomnameElement->FirstChildElement(); textElement; textElement = textElement->NextSiblingElement())
{
if (SDL_strcmp(textElement->Value(), "text") == 0)
{
const char* text = textElement->GetText();
if (text != NULL)
{
name.text.push_back(std::string(text));
}
}
}
}
map.specialroomnames.push_back(name);
}
}
}
if (mapwidth < maxwidth)
{
/* Unscramble platv, since it was stored incorrectly
* in 2.2 and previous... */
size_t i;
int x = 0;
int y = 0;
int temp_platv[numrooms];
for (i = 0; i < numrooms; ++i)
{
temp_platv[i] = roomproperties[i].platv;
}
for (i = 0; i < numrooms; ++i)
{
if (x < mapwidth)
{
const int platv_idx = x + y * mapwidth;
if (INBOUNDS_ARR(platv_idx, temp_platv))
{
roomproperties[i].platv = temp_platv[platv_idx];
}
}
else
{
roomproperties[i].platv = 4; /* default */
}
++x;
if (x >= maxwidth)
{
x = 0;
++y;
}
}
}
loc::loadtext_custom(_path.c_str());
font::load_custom(level_font_name.c_str());
version=2;
return true;
fail:
return false;
}
bool customlevelclass::save(const std::string& _path)
{
tinyxml2::XMLDocument doc;
std::string newpath("levels/" + _path);
// Try to preserve the XML of the currently-loaded one
bool already_exists = !ed.loaded_filepath.empty()
&& FILESYSTEM_loadTiXml2Document(ed.loaded_filepath.c_str(), doc);
if (!already_exists && !ed.loaded_filepath.empty())
{
vlog_error("Currently-loaded %s not found", ed.loaded_filepath.c_str());
}
ed.loaded_filepath = newpath;
tinyxml2::XMLElement* msg;
xml::update_declaration(doc);
tinyxml2::XMLElement * root = xml::update_element(doc, "MapData");
root->SetAttribute("version",version);
xml::update_comment(root, " Save file ");
tinyxml2::XMLElement * data = xml::update_element(root, "Data");
msg = xml::update_element(data, "MetaData");
//getUser
xml::update_tag(msg, "Creator", creator.c_str());
xml::update_tag(msg, "Title", title.c_str());
xml::update_tag(msg, "Created", version);
xml::update_tag(msg, "Modified", modifier.c_str());
xml::update_tag(msg, "Modifiers", version);
xml::update_tag(msg, "Desc1", Desc1.c_str());
xml::update_tag(msg, "Desc2", Desc2.c_str());
xml::update_tag(msg, "Desc3", Desc3.c_str());
xml::update_tag(msg, "website", website.c_str());
if (onewaycol_override)
{
xml::update_tag(msg, "onewaycol_override", onewaycol_override);
}
else
{
// Delete the element. I could just delete one, but just to be sure,
// I will delete all of them if there are more than one
tinyxml2::XMLElement* element;
while ((element = msg->FirstChildElement("onewaycol_override"))
!= NULL)
{
doc.DeleteNode(element);
}
}
if (level_font_name != "" && level_font_name != "font")
{
xml::update_tag(msg, "font", level_font_name.c_str());
}
else
{
// Get rid of it completely, same as <onewaycol_override>
tinyxml2::XMLElement* element;
while ((element = msg->FirstChildElement("font")) != NULL)
{
doc.DeleteNode(element);
}
}
if (rtl)
{
xml::update_tag(msg, "rtl", rtl);
}
else
{
// Also get rid of this one...
tinyxml2::XMLElement* element;
while ((element = msg->FirstChildElement("rtl")) != NULL)
{
doc.DeleteNode(element);
}
}
xml::update_tag(data, "mapwidth", mapwidth);
xml::update_tag(data, "mapheight", mapheight);
xml::update_tag(data, "levmusic", levmusic);
//New save format
std::string contentsString="";
for(int y = 0; y < mapheight*30; y++ )
{
for(int x = 0; x < mapwidth*40; x++ )
{
contentsString += help.String(getabstile(x, y)) + ",";
}
}
xml::update_tag(data, "contents", contentsString.c_str());
msg = xml::update_element_delete_contents(data, "edEntities");
for(size_t i = 0; i < customentities.size(); i++)
{
tinyxml2::XMLElement *edentityElement = doc.NewElement( "edentity" );
const int global_x = customentities[i].rx * SCREEN_WIDTH_TILES + customentities[i].x;
const int global_y = customentities[i].ry * SCREEN_HEIGHT_TILES + customentities[i].y;
edentityElement->SetAttribute("x", global_x);
edentityElement->SetAttribute("y", global_y);
edentityElement->SetAttribute( "t", customentities[i].t);
edentityElement->SetAttribute( "p1", customentities[i].p1);
edentityElement->SetAttribute( "p2", customentities[i].p2);
edentityElement->SetAttribute( "p3", customentities[i].p3);
edentityElement->SetAttribute( "p4", customentities[i].p4);
edentityElement->SetAttribute( "p5", customentities[i].p5);
edentityElement->SetAttribute( "p6", customentities[i].p6);
edentityElement->LinkEndChild( doc.NewText( customentities[i].scriptname.c_str() )) ;
msg->LinkEndChild( edentityElement );
}
msg = xml::update_element_delete_contents(data, "levelMetaData");
int temp_platv[numrooms];
for (size_t i = 0; i < SDL_arraysize(temp_platv); ++i)
{
temp_platv[i] = 4; /* default */
}
if (mapwidth < maxwidth)
{
/* Re-scramble platv, since it was stored incorrectly
* in 2.2 and previous... */
size_t i;
int x = 0;
int y = 0;
for (i = 0; i < numrooms; ++i)
{
if (x < mapwidth)
{
const int platv_idx = x + y * mapwidth;
if (INBOUNDS_ARR(platv_idx, temp_platv))
{
temp_platv[platv_idx] = roomproperties[i].platv;
}
}
++x;
if (x >= mapwidth)
{
/* Skip to next actual row. */
i += maxwidth - mapwidth;
x = 0;
++y;
}
}
}
for(size_t i = 0; i < SDL_arraysize(roomproperties); i++)
{
tinyxml2::XMLElement *roompropertyElement = doc.NewElement( "edLevelClass" );
roompropertyElement->SetAttribute( "tileset", roomproperties[i].tileset);
roompropertyElement->SetAttribute( "tilecol", roomproperties[i].tilecol);
roompropertyElement->SetAttribute( "platx1", roomproperties[i].platx1);
roompropertyElement->SetAttribute( "platy1", roomproperties[i].platy1);
roompropertyElement->SetAttribute( "platx2", roomproperties[i].platx2);
roompropertyElement->SetAttribute( "platy2", roomproperties[i].platy2);
roompropertyElement->SetAttribute( "platv", temp_platv[i]);
roompropertyElement->SetAttribute( "enemyx1", roomproperties[i].enemyx1);
roompropertyElement->SetAttribute( "enemyy1", roomproperties[i].enemyy1);
roompropertyElement->SetAttribute( "enemyx2", roomproperties[i].enemyx2);
roompropertyElement->SetAttribute( "enemyy2", roomproperties[i].enemyy2);
roompropertyElement->SetAttribute( "enemytype", roomproperties[i].enemytype);
roompropertyElement->SetAttribute( "directmode", roomproperties[i].directmode);
roompropertyElement->SetAttribute( "warpdir", roomproperties[i].warpdir);
roompropertyElement->LinkEndChild( doc.NewText( roomproperties[i].roomname.c_str() )) ;
msg->LinkEndChild( roompropertyElement );
}
std::string scriptString;
for(size_t i = 0; i < script.customscripts.size(); i++)
{
Script& script_ = script.customscripts[i];
scriptString += script_.name + ":|";
for (size_t ii = 0; ii < script_.contents.size(); ++ii)
{
scriptString += script_.contents[ii];
// Inserts a space if the line ends with a :
if (script_.contents[ii].length() && *script_.contents[ii].rbegin() == ':')
{
scriptString += " ";
}
scriptString += "|";
}
}
xml::update_tag(data, "script", scriptString.c_str());
return FILESYSTEM_saveTiXml2Document(newpath.c_str(), doc);
}
void customlevelclass::generatecustomminimap(void)
{
map.customzoom = 1;
if (mapwidth <= 10 && mapheight <= 10)
{
map.customzoom = 2;
}
if (mapwidth <= 5 && mapheight <= 5)
{
map.customzoom = 4;
}
// Set minimap offsets
switch (map.customzoom)
{
case 4:
map.custommmxoff = 24 * (5 - mapwidth);
map.custommmyoff = 18 * (5 - mapheight);
break;
case 2:
map.custommmxoff = 12 * (10 - mapwidth);
map.custommmyoff = 9 * (10 - mapheight);
break;
default:
map.custommmxoff = 6 * (20 - mapwidth);
map.custommmyoff = int(4.5 * (20 - mapheight));
break;
}
map.custommmxsize = 240 - (map.custommmxoff * 2);
map.custommmysize = 180 - (map.custommmyoff * 2);
// Start drawing the minimap
SDL_Texture* target = SDL_GetRenderTarget(gameScreen.m_renderer);
graphics.set_render_target(graphics.images[IMAGE_CUSTOMMINIMAP]);
graphics.clear();
// Scan over the map size
for (int j2 = 0; j2 < mapheight; j2++)
{
for (int i2 = 0; i2 < mapwidth; i2++)
{
std::vector<SDL_Point> dark_points;
std::vector<SDL_Point> light_points;
bool dark = getroomprop(i2, j2)->tileset == 1;
// Ok, now scan over each square
for (int j = 0; j < 9 * map.customzoom; j++)
{
for (int i = 0; i < 12 * map.customzoom; i++)
{
int tile;
switch (map.customzoom)
{
case 4:
tile = absfree(
int(i * 0.83) + (i2 * 40),
int(j * 0.83) + (j2 * 30)
);
break;
case 2:
tile = absfree(
int(i * 1.6) + (i2 * 40),
int(j * 1.6) + (j2 * 30)
);
break;
default:
tile = absfree(
3 + (i * 3) + (i2 * 40),
(j * 3) + (j2 * 30)
);
break;
}
if (tile >= 1)
{
// Add this pixel
SDL_Point point = { (i2 * 12 * map.customzoom) + i, (j2 * 9 * map.customzoom) + j };
if (dark)
{
dark_points.push_back(point);
}
else
{
light_points.push_back(point);
}
}
}
}
// Draw them all at once
if (!dark_points.empty())
{
graphics.draw_points(dark_points.data(), dark_points.size(), 96, 96, 96);
}
if (!light_points.empty())
{
graphics.draw_points(light_points.data(), light_points.size(), 196, 196, 196);
}
}
}
graphics.set_render_target(target);
}
// Return a graphics-ready color based off of the given tileset and tilecol
// Much kudos to Dav999 for saving me a lot of work, because I stole these colors from const.lua in Ved! -Info Teddy
SDL_Color customlevelclass::getonewaycol(const int rx, const int ry)
{
const RoomProperty* const room = getroomprop(rx, ry);
switch (room->tileset) {
case 0: // Space Station
switch (room->tilecol) {
case -1:
return graphics.getRGB(109, 109, 109);
case 0:
return graphics.getRGB(131, 141, 235);
case 1:
return graphics.getRGB(227, 140, 227);
case 2:
return graphics.getRGB(242, 126, 151);
case 3:
return graphics.getRGB(229, 235, 133);
case 4:
return graphics.getRGB(148, 238, 130);
case 5:
return graphics.getRGB(140, 165, 227);
case 6:
return graphics.getRGB(227, 140, 148);
case 7:
return graphics.getRGB(140, 173, 228);
case 8:
return graphics.getRGB(142, 235, 137);
case 9:
return graphics.getRGB(137, 235, 206);
case 10:
return graphics.getRGB(235, 139, 223);
case 11:
return graphics.getRGB(238, 130, 138);
case 12:
return graphics.getRGB(137, 235, 178);
case 13:
return graphics.getRGB(125, 205, 247);
case 14:
return graphics.getRGB(190, 137, 235);
case 15:
return graphics.getRGB(235, 137, 206);
case 16:
return graphics.getRGB(229, 247, 127);
case 17:
return graphics.getRGB(127, 200, 247);
case 18:
return graphics.getRGB(197, 137, 235);
case 19:
return graphics.getRGB(235, 131, 175);
case 20:
return graphics.getRGB(242, 210, 123);
case 21:
return graphics.getRGB(131, 235, 158);
case 22:
return graphics.getRGB(242, 126, 151);
case 23:
return graphics.getRGB(219, 243, 123);
case 24:
return graphics.getRGB(131, 234, 145);
case 25:
return graphics.getRGB(131, 199, 234);
case 26:
return graphics.getRGB(141, 131, 234);
case 27:
return graphics.getRGB(226, 140, 144);
case 28:
return graphics.getRGB(129, 236, 144);
case 29:
return graphics.getRGB(235, 231, 131);
case 30:
return graphics.getRGB(153, 235, 131);
case 31:
return graphics.getRGB(207, 131, 235);
}
break;
case 1: // Outside
switch (room->tilecol) {
case 0:
return graphics.getRGB(57, 86, 140);
case 1:
return graphics.getRGB(156, 42, 42);
case 2:
return graphics.getRGB(42, 156, 155);
case 3:
return graphics.getRGB(125, 36, 162);
case 4:
return graphics.getRGB(191, 198, 0);
case 5:
return graphics.getRGB(0, 198, 126);
case 6:
return graphics.getRGB(224, 110, 177);
case 7:
return graphics.getRGB(255, 142, 87);
}
break;
case 2: // Lab
switch (room->tilecol) {
case 0:
return graphics.getRGB(0, 165, 206);
case 1:
return graphics.getRGB(206, 5, 0);
case 2:
return graphics.getRGB(222, 0, 173);
case 3:
return graphics.getRGB(27, 67, 255);
case 4:
return graphics.getRGB(194, 206, 0);
case 5:
return graphics.getRGB(0, 206, 39);
case 6:
return graphics.getRGB(0, 165, 206);
}
break;
case 3: // Warp Zone
switch (room->tilecol) {
case 0:
return graphics.getRGB(113, 178, 197);
case 1:
return graphics.getRGB(197, 113, 119);
case 2:
return graphics.getRGB(196, 113, 197);
case 3:
return graphics.getRGB(149, 113, 197);
case 4:
return graphics.getRGB(197, 182, 113);
case 5:
return graphics.getRGB(141, 197, 113);
case 6:
return graphics.getRGB(109, 109, 109);
}
break;
case 4: // Ship
switch (room->tilecol) {
case 0:
return graphics.getRGB(0, 206, 39);
case 1:
return graphics.getRGB(0, 165, 206);
case 2:
return graphics.getRGB(194, 206, 0);
case 3:
return graphics.getRGB(206, 0, 160);
case 4:
return graphics.getRGB(27, 67, 255);
case 5:
return graphics.getRGB(206, 5, 0);
}
break;
}
// Uh, I guess return solid white
return graphics.getRGB(255, 255, 255);
}
// This version detects the room automatically
SDL_Color customlevelclass::getonewaycol(void)
{
if (game.gamestate == EDITORMODE)
{
return getonewaycol(ed.levx, ed.levy);
}
else if (map.custommode)
{
return getonewaycol(game.roomx - 100, game.roomy - 100);
}
// Uh, I guess return solid white
return graphics.getRGB(255, 255, 255);
}
static SDL_INLINE bool inbounds(const CustomEntity* entity)
{
extern customlevelclass cl;
return entity->x >= 0
&& entity->y >= 0
&& entity->x < cl.mapwidth * SCREEN_WIDTH_TILES
&& entity->y < cl.mapheight * SCREEN_HEIGHT_TILES;
}
int customlevelclass::numtrinkets(void)
{
int temp = 0;
for (size_t i = 0; i < customentities.size(); i++)
{
if (customentities[i].t == 9 && inbounds(&customentities[i]))
{
temp++;
}
}
return temp;
}
int customlevelclass::numcrewmates(void)
{
int temp = 0;
for (size_t i = 0; i < customentities.size(); i++)
{
if (customentities[i].t == 15 && inbounds(&customentities[i]))
{
temp++;
}
}
return temp;
}