foxygit / GZDoom Log in
commits tags

/src/wi_stuff.cpp · 35.39 KB

raw
/*
** wi_stuff.cpp
** Support code for intermission status screens
**
**---------------------------------------------------------------------------
** Copyright 2003-2017 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 <ctype.h>
#include <stdio.h>

#include "m_random.h"
#include "m_swap.h"

#include "filesystem.h"
#include "g_level.h"
#include "s_sound.h"
#include "doomstat.h"
#include "v_video.h"
#include "i_video.h"
#include "wi_stuff.h"
#include "hu_stuff.h"
#include "s_sndseq.h"
#include "v_text.h"
#include "gi.h"
#include "d_player.h"
#include "d_netinf.h"
#include "cmdlib.h"
#include "g_levellocals.h"
#include "vm.h"
#include "texturemanager.h"
#include "v_draw.h"

#include "serializer.h"
#include "s_music.h"

CVAR(Bool, wi_percents, true, CVAR_ARCHIVE)
CVAR(Bool, wi_showtotaltime, true, CVAR_ARCHIVE)
CVAR(Bool, wi_noautostartmap, false, CVAR_USERINFO | CVAR_ARCHIVE)
CVAR(Int, wi_autoadvance, 0, CVAR_SERVERINFO)
CVAR(Bool, wi_cleantextscale, false, CVAR_ARCHIVE)
EXTERN_CVAR(Bool, inter_classic_scaling)

// States for the intermission
enum EState
{
	NoState = -1,
	StatCount,
	ShowNextLoc,
	LeavingIntermission
};

static const char *WI_Cmd[] = {
	"Background",
	"Splat",
	"Pointer",
	"Spots",

	"IfEntering",
	"IfNotEntering",
	"IfVisited",
	"IfNotVisited",
	"IfLeaving",
	"IfNotLeaving",
	"IfTravelling",
	"IfNotTravelling",

	"Animation",
	"Pic",

	"NoAutostartMap",
	"Screensize",
	"TileBackground",

	NULL
};

class DInterBackground : public DObject
{
	DECLARE_ABSTRACT_CLASS(DInterBackground, DObject)
	
	// These animation variables, structures, etc. are used for the
	// DOOM/Ultimate DOOM intermission screen animations.  This is
	// totally different from any sprite or texture/flat animations
	enum EAnim
	{
		ANIM_ALWAYS,	// determined by patch entry
		ANIM_PIC,		// continuous
		ANIM_FRAME,     // determined by frame properties
		ANIM_NONE,      // frozen (used for infinite duration frames)

		// condition bitflags
		ANIM_IFVISITED = 8,
		ANIM_IFNOTVISITED = 16,
		ANIM_IFENTERING = 32,
		ANIM_IFNOTENTERING = 64,
		ANIM_IFLEAVING = 128,
		ANIM_IFNOTLEAVING = 256,
		ANIM_IFTRAVELLING = 512,
		ANIM_IFNOTTRAVELLING = 1024,

		ANIM_TYPE = 7,
		ANIM_CONDITION = ~7,

	};

	struct yahpt_t
	{
		int x, y;
	};

	struct lnode_t
	{
		int   x;       // x/y coordinate pair structure
		int   y;
		FString Level;
	};

	struct frame_t
	{
		FTextureID	image;
		uint32_t	type;
		// In tics!
		int         duration;
		int         max_duration;
	};

	enum ECondition
	{
		COND_NONE = 0, // None.
		COND_GREATER = 1, // Parameter is greater than current map number.
		COND_EQUAL = 2, // Parameter is equal to current map number.
		COND_VISITED = 3, // Map number corresponding to parameter is visited.
		COND_NOTSECRET = 4, // Current map is not a secret one.
		COND_SECRETVISTED = 5, // Any secret map was visited.
		COND_TALLY = 6, // Tally screen.
		COND_ENTERING = 7, // "Entering" screen.
	};

	struct condition_t
	{
		ECondition condition;
		int param;
	};

	struct in_anim_t
	{
		int			type;	// Made an int so I can use '|'
		int 		period;	// period in tics between animations
		yahpt_t 	loc;	// location of animation
		int 		data;	// ALWAYS: n/a, RANDOM: period deviation (<256)
		TArray<frame_t>	frames;	// actual graphics for frames of animations
		TArray<condition_t> conditions; // conditions to display the animation

									// following must be initialized to zero before use!
		int 		nexttic;	// next value of bcnt (used in conjunction with period)
		int 		ctr;		// next frame number to animate
		int 		state;		// used by RANDOM and LEVEL when animating

		FString		LevelName;
		FString		LevelName2;

		void Reset()
		{
			type = period = loc.x = loc.y = data = nexttic = ctr = state = 0;
			LevelName = "";
			LevelName2 = "";
			frames.Clear();
		}
	};

	struct in_layer_t
	{
		TArray<in_anim_t> anims;
		TArray<condition_t> conditions; // conditions to display the animations.
	};

private:
	TArray<lnode_t> lnodes;
	TArray<in_layer_t> layers;
	int				bcnt = 0;				// used for timing of background animation
	TArray<FTextureID> yah; 		// You Are Here graphic
	FTextureID		splat{};		// splat
	FTextureID		background{};
	wbstartstruct_t *wbs;
	level_info_t	*exitlevel;
	int			bgwidth = -1;
	int			bgheight = -1;
	bool		tilebackground = false;
	FString		muslump;


public:

	DInterBackground(wbstartstruct_t *wbst);
	bool LoadBackground(bool isenterpic);
	bool IsUsingMusic();
	void updateAnimatedBack();
	void drawBackground(int state, bool drawsplat, bool snl_pointeron);

private:

	bool IsExMy(const char * name)
	{
		// Only check for the first 3 episodes. They are the only ones with default intermission scripts.
		// Level names can be upper- and lower case so use tolower to check.
		return (tolower(name[0]) == 'e' && name[1] >= '1' && name[1] <= '3' && tolower(name[2]) == 'm');
	}
	
	//====================================================================
	//
	// Draws the splats and the 'You are here' arrows
	//
	//====================================================================

	int MapToIndex(const char *map)
	{
		unsigned int i;

		for (i = 0; i < lnodes.Size(); i++)
		{
			if (!lnodes[i].Level.CompareNoCase(map))
				break;
		}
		return i;
	}


	//====================================================================
	//
	// Draws the splats and the 'You are here' arrows
	//
	//====================================================================

	void drawOnLnode(int   n, FTextureID c[], int numc, double backwidth, double backheight)
	{
		int   i;
		for (i = 0; i<numc; i++)
		{
			double            left;
			double            top;
			double            right;
			double            bottom;

			auto tex = TexMan.GetGameTexture(c[i]);
			right = tex->GetDisplayWidth();
			bottom = tex->GetDisplayHeight();
			left = lnodes[n].x - tex->GetDisplayLeftOffset();
			top = lnodes[n].y - tex->GetDisplayTopOffset();
			right += left;
			bottom += top;

			if (left >= 0 && right < backwidth && top >= 0 && bottom < backheight)
			{
				DrawTexture(twod, tex, lnodes[n].x, lnodes[n].y, DTA_FullscreenScale, FSMode_ScaleToFit43, DTA_VirtualWidthF, backwidth, DTA_VirtualHeightF, backheight, TAG_DONE);
				break;
			}
		}
	}

	//====================================================================
	//
	// Draws the splats and the 'You are here' arrows
	//
	//====================================================================
	bool ConditionsMet(int state, TArray<condition_t>& conditions)
	{
		for (auto& condition : conditions)
		{
			switch (condition.condition)
			{
				case ECondition::COND_NONE:
					break;
				case ECondition::COND_EQUAL:
				{
					auto* li = FindLevelInfo(state != StatCount ? wbs->next.GetChars() : wbs->current.GetChars());
					if (!li)
						return false;
					int levelnum = li->id24_levelnum ? li->id24_levelnum : li->levelnum;
					if (levelnum != condition.param)
						return false;
					break;
				}
				case ECondition::COND_GREATER:
				{
					auto* li = FindLevelInfo(state != StatCount ? wbs->next.GetChars() : wbs->current.GetChars());
					if (!li)
						return false;
					int levelnum = li->id24_levelnum ? li->id24_levelnum : li->levelnum;
					if (levelnum != condition.param)
						return false;
					break;
				}
				case ECondition::COND_VISITED:
				{
					auto* li = FindLevelByNum(condition.param);
					if (li == NULL || !(li->flags & LEVEL_VISITED))
						return false;
					break;
				}
				case ECondition::COND_NOTSECRET:
				{
					auto* li = FindLevelInfo(state != StatCount ? wbs->next.GetChars() : wbs->current.GetChars());
					if (!li)
						return false;
					if (li->flags3 & LEVEL3_SECRET)
						return false;
					break;
				}
				case ECondition::COND_SECRETVISTED:
					if (!SecretLevelVisited())
						return false;
					break;
				case ECondition::COND_TALLY:
					if (state != StatCount)
						return false;
					break;
				case ECondition::COND_ENTERING:
					if (state == StatCount)
						return false;
					break;
			}
		}
		return true;
	}
};

DInterBackground:: DInterBackground(wbstartstruct_t *wbst)
{
	wbs = wbst;
}

DEFINE_ACTION_FUNCTION(DInterBackground, Create)
{
	PARAM_PROLOGUE;
	PARAM_POINTER(wbst, wbstartstruct_t);
	ACTION_RETURN_POINTER(Create<DInterBackground>(wbst));
}

//====================================================================
// 
//	Loads the background - either from a single texture
//	or an intermission lump.
//	Unfortunately the texture manager is incapable of recognizing text
//	files so if you use a script you have to prefix its name by '$' in 
//  MAPINFO.
//
//====================================================================

bool DInterBackground::LoadBackground(bool isenterpic)
{
	const char* lumpname = nullptr;
	const char* exitpic = nullptr;
	const char* exitanim = nullptr;
	char buffer[10];
	in_anim_t an;
	lnode_t pt;
	FTextureID texture;
	bool noautostartmap = false;
	bool id24anim = false;

	bcnt = 0;

	if (!isenterpic) tilebackground = false;
	texture.SetInvalid();

	level_info_t* li = FindLevelInfo(wbs->current.GetChars());
	if (li != nullptr)
	{
		if (li->ExitAnim.IsNotEmpty())
		{
			id24anim = true;
			exitpic = li->ExitAnim.GetChars();
			tilebackground = false;
		}
		else
		{
			exitpic = li->ExitPic.GetChars();
			if (li->ExitPic.IsNotEmpty()) tilebackground = false;
		}
	}
	lumpname = exitpic;

	if (isenterpic)
	{
		level_info_t* li = FindLevelInfo(wbs->next.GetChars());
		if (li != NULL)
		{
			if (li->EnterAnim.IsNotEmpty())
			{
				id24anim = true;
				lumpname = li->EnterAnim.GetChars();
				tilebackground = false;
			}
			else
			{
				lumpname = li->EnterPic.GetChars();
				if (li->EnterPic.IsNotEmpty()) tilebackground = false;
			}
		}
	}

	// Try to get a default if nothing specified
	if (lumpname == NULL || lumpname[0] == 0)
	{
		lumpname = NULL;
		switch (gameinfo.gametype)
		{
		case GAME_Chex:
		case GAME_Doom:
			if (!(gameinfo.flags & GI_MAPxx))
			{
				const char* levelname = isenterpic ? wbs->next.GetChars() : wbs->current.GetChars();
				if (IsExMy(levelname))
				{
					mysnprintf(buffer, countof(buffer), "$IN_EPI%c", levelname[1]);
					lumpname = buffer;
					tilebackground = false;
				}
			}
			if (!lumpname)
			{
				if (isenterpic)
				{
					// One special case needs to be handled here:
					// If going from E1-E3 to E4 the default should be used, not the exit pic.

					// Not if the exit pic is user defined.
					if (exitpic && exitpic[0]) return false;

					// E1-E3 need special treatment when playing Doom 1.
					if (!(gameinfo.flags & GI_MAPxx))
					{
						// not if the last level is not from the first 3 episodes
						if (!IsExMy(wbs->current.GetChars())) return false;

						// not if the next level is one of the first 3 episodes
						if (IsExMy(wbs->next.GetChars())) return false;
					}
				}
				lumpname = "INTERPIC";
				tilebackground = false;
			}
			break;

		case GAME_Heretic:
			if (isenterpic)
			{
				if (IsExMy(wbs->next.GetChars()))
				{
					mysnprintf(buffer, countof(buffer), "$IN_HTC%c", wbs->next[1]);
					lumpname = buffer;
					tilebackground = false;
				}
			}
			if (!lumpname)
			{
				if (isenterpic) return false;
				lumpname = "FLOOR16";
				tilebackground = true;
			}
			break;

		case GAME_Hexen:
			if (isenterpic) return false;
			lumpname = "INTERPIC";
			break;

		case GAME_Strife:
		default:
			// Strife doesn't have an intermission pic so choose something neutral.
			if (isenterpic) return false;
			lumpname = gameinfo.BorderFlat.GetChars();
			tilebackground = true;
			break;
		}
	}
	if (lumpname == NULL)
	{
		// shouldn't happen!
		background.SetInvalid();
		return false;
	}

	lnodes.Clear();
	layers.Clear();
	yah.Clear();
	splat.SetInvalid();
	if (!id24anim)
		layers.Resize(1);

	if (id24anim)
	{
		try
		{
			int lumpnum = fileSystem.CheckNumForFullName(lumpname, true);
			if (lumpnum == -1)
			{
				I_Error("Intermission animation lump %s not found!", lumpname);
			}
			auto data = fileSystem.ReadFile(lumpnum);
			FSerializer jsonReader;
			jsonReader.mLumpName = fileSystem.GetFileFullPath(lumpnum);
			lumpname = jsonReader.mLumpName.GetChars();
			if (jsonReader.OpenReader(data.string(), data.size()))
			{
				FString type = jsonReader.GetString("type");
				if (type.Compare("interlevel"))
				{
					I_Error("Interlevel lump %s is not interlevel!", lumpname);
				}
				if (jsonReader.BeginObject("data"))
				{
					FString music = jsonReader.GetString("music");
					if (music.IsEmpty())
					{
						I_Error("No music lump specified for intermission animation %s!", lumpname);
					}
					muslump = music;
					if (!MusicExists(muslump.GetChars()))
					{
						I_Error("Music lump %s not found!", muslump.GetChars());
					}
				
					// Check for background lump.
					FString backgroundimage = jsonReader.GetString("backgroundimage");
					texture = TexMan.CheckForTexture(backgroundimage.GetChars(), ETextureType::MiscPatch, FTextureManager::TEXMAN_TryAny);
					if (!texture.isValid())
					{
						if (backgroundimage.IsEmpty())
						{
							I_Error("No background image specified for intermission animation %s!", lumpname);
						}
						else
						{
							I_Error("Texture %s not found!", backgroundimage.GetChars());
						}
					}

					if (!jsonReader.IsKeyNull("layers") && jsonReader.BeginArray("layers"))
					{
						int size = jsonReader.ArraySize();

						if (size == 0)
							I_Error("Zero-length 'layers' array in %s", lumpname);

						for (int i = 0; i < size; i++)
						{
							if (jsonReader.BeginObject(nullptr))
							{
								layers.Reserve(1);
								auto& layer = layers.back();
								if (!jsonReader.IsKeyNull("conditions") && jsonReader.BeginArray("conditions"))
								{
									int condition_size = jsonReader.ArraySize();
									if (condition_size == 0)
									{
										I_Error("Condition array empty and not null for layer %d in lump %s", layers.Size(), lumpname);
									}
									for (int j = 0; j < condition_size; j++)
									{
										if (jsonReader.BeginObject(nullptr))
										{
											ECondition cond = COND_NONE;
											int param = 0;
											::Serialize(jsonReader, "condition", (int&)cond, nullptr);
											::Serialize(jsonReader, "param", param, nullptr);
											layer.conditions.Push(condition_t{ cond, param });
											jsonReader.EndObject();
										}
									}
									jsonReader.EndArray();
								}

								// Read anims.
								if (jsonReader.BeginArray("anims"))
								{
									int anim_size = jsonReader.ArraySize();
									if (anim_size == 0)
									{
										I_Error("No animations defined for layer %d in lump %s", layers.Size(), lumpname);
									}

									for (int j = 0; j < anim_size; j++)
									{
										if (jsonReader.BeginObject(nullptr))
										{
											layer.anims.Reserve(1);
											auto& anim = layer.anims.back();

											anim.Reset();
											anim.type = ANIM_FRAME;
											anim.ctr = 0;
											anim.data = 0;
										
											::Serialize(jsonReader, "x", anim.loc.x, nullptr);
											::Serialize(jsonReader, "y", anim.loc.y, nullptr);

											if (!jsonReader.IsKeyNull("conditions") && jsonReader.BeginArray("conditions"))
											{
												int condition_size = jsonReader.ArraySize();
												if (condition_size == 0)
												{
													I_Error("Condition array empty and not null for anim %d, layer %d in lump %s", layer.anims.Size(), layers.Size(), lumpname);
												}
												for (int k = 0; k < condition_size; k++)
												{
													if (jsonReader.BeginObject(nullptr))
													{
														ECondition cond = COND_NONE;
														int param = 0;
														::Serialize(jsonReader, "condition", (int&)cond, nullptr);
														::Serialize(jsonReader, "param", param, nullptr);
														anim.conditions.Push(condition_t{ cond, param });
														jsonReader.EndObject();
													}
												}
												jsonReader.EndArray();
											}

											if (jsonReader.BeginArray("frames"))
											{
												int frames = jsonReader.ArraySize();

												if (frames == 0)
												{
													I_Error("No frames defined for anim %d, layer %d in lump %s", layer.anims.Size(), layers.Size(), lumpname);
												}
												for (int k = 0; k < frames; k++)
												{
													if (jsonReader.BeginObject(nullptr))
													{
														double duration = 0.0;
														double maxduration = 0.0;
														anim.frames.Reserve(1);
														auto& frame = anim.frames.back();

														::Serialize(jsonReader, "duration", duration, nullptr);
														::Serialize(jsonReader, "maxduration", maxduration, nullptr);
														::Serialize(jsonReader, "type", frame.type, nullptr);

														FString image = jsonReader.GetString("image");
														if (image.IsEmpty())
														{
															I_Error("No image defined for frame %d, anim %d, layer %d in lump %s", anim.frames.Size(), layer.anims.Size(), layers.Size(), lumpname);
														}

														frame.image = TexMan.CheckForTexture(image.GetChars(), ETextureType::MiscPatch, FTextureManager::TEXMAN_TryAny);
														if (!frame.image.isValid())
														{
															I_Error("Texture '%s' not found!", image.GetChars());
														}

														frame.duration = round(duration * (double)TICRATE);
														frame.max_duration = round(maxduration * (double)TICRATE);

														if (frame.duration <= 0 && duration > 0.0)
														{
															frame.duration = 1;
														}

														if (frame.max_duration <= 0 && maxduration > 0.0)
														{
															frame.max_duration = 1;
														}

														if (anim.frames.Size() == 1)
														{
															if (frame.type & 0x1000)
															{
																frame.type &= ~0x1000;
																anim.nexttic = 1 + (M_Random() % frame.duration);
															}
															else
															{

																switch (frame.type)
																{
																case 0x1:
																	anim.type = ANIM_NONE;
																	break;
																case 0x4:
																	anim.nexttic = 1 + M_Random(frame.max_duration - frame.duration + 1) + frame.duration;
																	break;
																case 0x2:
																	anim.nexttic = 1 + frame.duration;
																	break;
																}
															}
														}

														jsonReader.EndObject();
													}
												}
												jsonReader.EndArray();
											}

											jsonReader.EndObject();
										}
									}

									jsonReader.EndArray();
								}

								jsonReader.EndObject();
							}
						}
						jsonReader.EndArray();
					}

					jsonReader.EndObject();
				}
				jsonReader.Close();
			}
		}
		// This is deliberate. Errors coming from here shouldn't cause a console abort.
		catch (CRecoverableError& error)
		{
			Printf(TEXTCOLOR_RED "Failed to parse intermission anim definition %s: %s.\nFalling back to non-anim definitions.\n", lumpname, error.GetMessage());

			if (isenterpic)
				li->EnterAnim = "";
			else
				li->ExitAnim = "";

			return LoadBackground(isenterpic);
		}
	}
	// a name with a starting '$' indicates an intermission script
	else if (*lumpname != '$')
	{
		texture = TexMan.CheckForTexture(lumpname, ETextureType::MiscPatch, FTextureManager::TEXMAN_TryAny);
	}
	else
	{
		int lumpnum = fileSystem.CheckNumForFullName(lumpname + 1, true);
		if (lumpnum >= 0)
		{
			FScanner sc(lumpnum);
			while (sc.GetString())
			{
				an.Reset();
				int caseval = sc.MustMatchString(WI_Cmd);
				switch (caseval)
				{
				case 0:		// Background
					sc.MustGetString();
					texture = TexMan.CheckForTexture(sc.String, ETextureType::MiscPatch, FTextureManager::TEXMAN_TryAny);
					break;

				case 1:		// Splat
					sc.MustGetString();
					splat = TexMan.CheckForTexture(sc.String, ETextureType::MiscPatch, FTextureManager::TEXMAN_TryAny);
					break;

				case 2:		// Pointers
					while (sc.GetString() && !sc.Crossed)
					{
						yah.Push(TexMan.CheckForTexture(sc.String, ETextureType::MiscPatch, FTextureManager::TEXMAN_TryAny));
					}
					if (sc.Crossed)
						sc.UnGet();
					break;

				case 3:		// Spots
					sc.MustGetStringName("{");
					while (!sc.CheckString("}"))
					{
						sc.MustGetString();
						pt.Level = sc.String;
						sc.MustGetNumber();
						pt.x = sc.Number;
						sc.MustGetNumber();
						pt.y = sc.Number;
						lnodes.Push(pt);
					}
					break;

				case 4:		// IfEntering
					an.type = ANIM_IFENTERING;
					goto readanimation;

				case 5:		// IfEntering
					an.type = ANIM_IFNOTENTERING;
					goto readanimation;

				case 6:		// IfVisited
					an.type = ANIM_IFVISITED;
					goto readanimation;

				case 7:		// IfNotVisited
					an.type = ANIM_IFNOTVISITED;
					goto readanimation;

				case 8:		// IfLeaving
					an.type = ANIM_IFLEAVING;
					goto readanimation;

				case 9:		// IfNotLeaving
					an.type = ANIM_IFNOTLEAVING;
					goto readanimation;

				case 10:	// IfTravelling
					an.type = ANIM_IFTRAVELLING;
					sc.MustGetString();
					an.LevelName2 = sc.String;
					goto readanimation;

				case 11:	// IfNotTravelling
					an.type = ANIM_IFTRAVELLING;
					sc.MustGetString();
					an.LevelName2 = sc.String;
					goto readanimation;

				case 14:	// NoAutostartMap
					noautostartmap = true;
					break;

				case 15:	// screensize
					sc.MustGetNumber();
					bgwidth = sc.Number;
					sc.MustGetNumber();
					bgheight = sc.Number;
					break;

				case 16:	// tilebackground
					tilebackground = true;
					break;

				readanimation:
					sc.MustGetString();
					an.LevelName = sc.String;
					sc.MustGetString();
					caseval = sc.MustMatchString(WI_Cmd);
					[[fallthrough]];

				default:
					switch (caseval)
					{
					case 12:	// Animation
						an.type |= ANIM_ALWAYS;
						sc.MustGetNumber();
						an.loc.x = sc.Number;
						sc.MustGetNumber();
						an.loc.y = sc.Number;
						sc.MustGetNumber();
						an.period = sc.Number;
						an.nexttic = 1 + (M_Random() % an.period);
						if (sc.GetString())
						{
							if (sc.Compare("ONCE"))
							{
								an.data = 1;
							}
							else
							{
								sc.UnGet();
							}
						}
						if (!sc.CheckString("{"))
						{
							sc.MustGetString();
							auto textureId = TexMan.CheckForTexture(sc.String, ETextureType::MiscPatch, FTextureManager::TEXMAN_TryAny);
							an.frames.Push(frame_t{ textureId, 0, 0, 0 });
						}
						else
						{
							while (!sc.CheckString("}"))
							{
								sc.MustGetString();
								auto textureId = TexMan.CheckForTexture(sc.String, ETextureType::MiscPatch, FTextureManager::TEXMAN_TryAny);
								an.frames.Push(frame_t{ textureId, 0, 0, 0 });
							}
						}
						an.ctr = -1;
						layers[0].anims.Push(an);
						break;

					case 13:		// Pic
						an.type |= ANIM_PIC;
						sc.MustGetNumber();
						an.loc.x = sc.Number;
						sc.MustGetNumber();
						an.loc.y = sc.Number;
						sc.MustGetString();
						an.frames.Reserve(1);	// allocate exactly one element
						an.frames[0].image = TexMan.CheckForTexture(sc.String, ETextureType::MiscPatch, FTextureManager::TEXMAN_TryAny);
						layers[0].anims.Push(an);
						break;

					default:
						sc.ScriptError("Unknown token %s in intermission script", sc.String);
					}
				}
			}
		}
		else
		{
			
			Printf("Intermission script %s not found!\n", lumpname + 1);
			texture = TexMan.GetTextureID("INTERPIC", ETextureType::MiscPatch);
		}
	}
	background = texture;
	auto tex = TexMan.GetGameTexture(texture);
	// extremely small textures will always be tiled.
	if (tex && tex->GetDisplayWidth() < 128 && tex->GetDisplayHeight() < 128)
		tilebackground = true;
	return noautostartmap;
}

DEFINE_ACTION_FUNCTION(DInterBackground, LoadBackground)
{
	PARAM_SELF_PROLOGUE(DInterBackground);
	PARAM_BOOL(isenterpic);
	ACTION_RETURN_BOOL(self->LoadBackground(isenterpic));
}

//====================================================================
// 
//	made this more generic and configurable through a script
//	Removed all the ugly special case handling for different game modes
//
//====================================================================

void DInterBackground::updateAnimatedBack()
{
	unsigned int i;

	bcnt++;
	if (bcnt == 1 && muslump.IsNotEmpty())
	{
		S_ChangeMusic(muslump.GetChars());
	}

	for (auto& layer : layers)
	{
		auto& anims = layer.anims;
		for (i = 0; i < anims.Size(); i++)
		{
			in_anim_t* a = &anims[i];
			switch (a->type & ANIM_TYPE)
			{
			case ANIM_NONE:
				break;

			case ANIM_FRAME:
				if (bcnt >= a->nexttic)
				{
					if (++a->ctr >= (int)a->frames.Size())
						a->ctr = 0;

					switch (a->frames[a->ctr].type & 0x7) {
					case 0x1:
						a->type &= ANIM_CONDITION;
						a->type = ANIM_NONE;
						break;
					case 0x4:
						a->nexttic = bcnt + M_Random(a->frames[a->ctr].max_duration - a->frames[a->ctr].duration + 1) + a->frames[a->ctr].duration;
						break;
					case 0x2:
						a->nexttic = bcnt + a->frames[a->ctr].duration;
						break;
					}
				}
				break;
			case ANIM_ALWAYS:
				if (bcnt >= a->nexttic)
				{
					if (++a->ctr >= (int)a->frames.Size())
					{
						if (a->data == 0) a->ctr = 0;
						else a->ctr--;
					}
					a->nexttic = bcnt + a->period;
				}
				break;

			case ANIM_PIC:
				a->ctr = 0;
				break;

			}
		}
	}
}

DEFINE_ACTION_FUNCTION(DInterBackground, updateAnimatedBack)
{
	PARAM_SELF_PROLOGUE(DInterBackground);
	self->updateAnimatedBack();
	return 0;
}

//====================================================================
// 
//	Draws the background including all animations
//
//====================================================================

void DInterBackground::drawBackground(int state, bool drawsplat, bool snl_pointeron)
{
	unsigned int i;
	double animwidth = bgwidth;		// For a flat fill or clear background scale animations to 320x200
	double animheight = bgheight;

	if (background.isValid())
	{
		auto bgtex = TexMan.GetGameTexture(background);
		// background
		if (!tilebackground)
		{
			// if no explicit size was set scale all animations below to fit the size of the base pic
			// The base pic is always scaled to fit the screen so this allows
			// placing the animations precisely where they belong on the base pic
			if (bgwidth < 0 || bgheight < 0)
			{
				animwidth = bgtex->GetDisplayWidth();
				animheight = bgtex->GetDisplayHeight();
				if (animheight == 200) animwidth = 320;	// deal with widescreen replacements that keep the original coordinates.
			}
			DrawTexture(twod, bgtex, 0, 0, DTA_FullscreenEx, FSMode_ScaleToFit43, TAG_DONE);
		}
		else
		{
			twod->AddFlatFill(0, 0, twod->GetWidth(), twod->GetHeight(), bgtex, (inter_classic_scaling ? -1 : 0));
		}
	}
	else
	{
		ClearRect(twod, 0, 0, twod->GetWidth(), twod->GetHeight(), 0, 0);
	}

	for (auto& layer : layers)
	{
		auto& anims = layer.anims;

		if (!ConditionsMet(state, layer.conditions))
			continue;

		for (i = 0; i < anims.Size(); i++)
		{
			in_anim_t* a = &anims[i];
			level_info_t* li;

			switch (a->type & ANIM_CONDITION)
			{
			case ANIM_IFVISITED:
				li = FindLevelInfo(a->LevelName.GetChars());
				if (li == NULL || !(li->flags & LEVEL_VISITED)) continue;
				break;

			case ANIM_IFNOTVISITED:
				li = FindLevelInfo(a->LevelName.GetChars());
				if (li == NULL || (li->flags & LEVEL_VISITED)) continue;
				break;

				// StatCount means 'leaving' - everything else means 'entering'!
			case ANIM_IFENTERING:
				if (state == StatCount || a->LevelName.CompareNoCase(wbs->next, 8)) continue;
				break;

			case ANIM_IFNOTENTERING:
				if (state != StatCount && !a->LevelName.CompareNoCase(wbs->next, 8)) continue;
				break;

			case ANIM_IFLEAVING:
				if (state != StatCount || a->LevelName.CompareNoCase(wbs->current, 8)) continue;
				break;

			case ANIM_IFNOTLEAVING:
				if (state == StatCount && !a->LevelName.CompareNoCase(wbs->current, 8)) continue;
				break;

			case ANIM_IFTRAVELLING:
				if (a->LevelName2.CompareNoCase(wbs->current, 8) || a->LevelName.CompareNoCase(wbs->next, 8)) continue;
				break;

			case ANIM_IFNOTTRAVELLING:
				if (!a->LevelName2.CompareNoCase(wbs->current, 8) && !a->LevelName.CompareNoCase(wbs->next, 8)) continue;
				break;
			}
			if (a->ctr >= 0 && ConditionsMet(state, a->conditions))
				DrawTexture(twod, a->frames[a->ctr].image, false, a->loc.x, a->loc.y,
					DTA_VirtualWidthF, animwidth, DTA_VirtualHeightF, animheight, DTA_FullscreenScale, FSMode_ScaleToFit43, TAG_DONE);
		}
	}

	if (drawsplat)
	{
		for (i = 0; i<lnodes.Size(); i++)
		{
			level_info_t * li = FindLevelInfo(lnodes[i].Level.GetChars());
			if (li && li->flags & LEVEL_VISITED) drawOnLnode(i, &splat, 1, animwidth, animheight);  // draw a splat on taken cities.
		}
	}

	// draw flashing ptr
	if (snl_pointeron && yah.Size())
	{
		unsigned int v = MapToIndex(wbs->next.GetChars());
		// Draw only if it points to a valid level on the current screen!
		if (v<lnodes.Size()) drawOnLnode(v, &yah[0], yah.Size(), animwidth, animheight);
	}
}

DEFINE_ACTION_FUNCTION(DInterBackground, drawBackground)
{
	PARAM_SELF_PROLOGUE(DInterBackground);
	PARAM_INT(state);
	PARAM_BOOL(splat);
	PARAM_BOOL(pointer);
	self->drawBackground(state, splat, pointer);
	return 0;
}

//====================================================================
//
//
//
//====================================================================

bool DInterBackground::IsUsingMusic()
{
	return muslump.IsNotEmpty();
}

DEFINE_ACTION_FUNCTION(DInterBackground, IsUsingMusic)
{
	PARAM_SELF_PROLOGUE(DInterBackground);
	ACTION_RETURN_BOOL(self->IsUsingMusic());
}

IMPLEMENT_CLASS(DInterBackground, true, false)

//====================================================================
// 
// Setup for an intermission screen.
//
//====================================================================

DObject* WI_Start(wbstartstruct_t *wbstartstruct)
{
	FName screenclass = deathmatch ? gameinfo.statusscreen_dm : multiplayer ? gameinfo.statusscreen_coop : gameinfo.statusscreen_single;
	auto cls = PClass::FindClass(screenclass);

	if (cls == nullptr || !cls->IsDescendantOf("StatusScreen"))
	{
		// Name was invalid - pick some working default.
		Printf("Status screen class %s not found - reverting to default", screenclass.GetChars());
		screenclass = deathmatch ? NAME_DeathmatchStatusScreen : multiplayer ? NAME_CoopStatusScreen : NAME_RavenStatusScreen;
		cls = PClass::FindClass(screenclass);
		if (cls == nullptr)
		{
			I_FatalError("Cannot create status screen");
		}
	}
	
	auto WI_Screen = cls->CreateNew();


	ScaleOverrider s(twod);
	IFVIRTUALPTRNAME(WI_Screen, "StatusScreen", Start)
	{
		VMValue val[2] = { WI_Screen, wbstartstruct };
		VMCall(func, val, 2, nullptr, 0);
	}

	if (!wi_cleantextscale)
	{
		// Only modify the original single player screens. Everything else must set itself up as intended
		if (cls->TypeName == NAME_DoomStatusScreen || cls->TypeName == NAME_RavenStatusScreen)
		{
			int w = screen->GetWidth();
			int h = screen->GetHeight();
			float ratio = ActiveRatio(w, h);
			int pixw = int(320 * (ratio * 0.75));
			if (pixw > 336) pixw -= 16;	// leave a bit of space at the sides.

			WI_Screen->IntVar(NAME_cwidth) = 320;
			WI_Screen->IntVar(NAME_cheight) = 200;
			WI_Screen->IntVar(NAME_scalemode) = FSMode_ScaleToFit43;
			WI_Screen->IntVar(NAME_scalefactorx) = 1;
			WI_Screen->IntVar(NAME_scalefactory) = 1;
			WI_Screen->IntVar(NAME_wrapwidth) = pixw;
		}
	}

	return WI_Screen;
}

//====================================================================
// 
//
//
//====================================================================

DEFINE_ACTION_FUNCTION(DStatusScreen, GetPlayerWidths)
{
	PARAM_PROLOGUE;
	int maxnamewidth, maxscorewidth, maxiconheight;
	HU_GetPlayerWidths(maxnamewidth, maxscorewidth, maxiconheight);
	if (numret > 0) ret[0].SetInt(maxnamewidth);
	if (numret > 1) ret[1].SetInt(maxscorewidth);
	if (numret > 2) ret[2].SetInt(maxiconheight);
	return min(numret, 3);
}

//====================================================================
// 
//
//
//====================================================================

DEFINE_ACTION_FUNCTION(DStatusScreen, GetRowColor)
{
	PARAM_PROLOGUE;
	PARAM_POINTER(p, player_t);
	PARAM_BOOL(highlight);
	ACTION_RETURN_INT(HU_GetRowColor(p, highlight));
}

//====================================================================
// 
//
//
//====================================================================

DEFINE_ACTION_FUNCTION(DStatusScreen, GetSortedPlayers)
{
	PARAM_PROLOGUE;
	PARAM_POINTER(array, TArray<int>);
	PARAM_BOOL(teamplay);

	player_t *sortedplayers[MAXPLAYERS];
	// Sort all players
	for (int i = 0; i < MAXPLAYERS; i++)
	{
		sortedplayers[i] = &players[i];
	}

	if (teamplay)
		qsort(sortedplayers, MAXPLAYERS, sizeof(player_t *), compareteams);
	else
		qsort(sortedplayers, MAXPLAYERS, sizeof(player_t *), comparepoints);

	array->Resize(MAXPLAYERS);
	for (unsigned i = 0; i < MAXPLAYERS; i++)
	{
		(*array)[i] = int(sortedplayers[i] - players);
	}
	return 0;
}

//====================================================================
// 
//
//
//====================================================================

DEFINE_FIELD_X(WBPlayerStruct, wbplayerstruct_t, skills);
DEFINE_FIELD_X(WBPlayerStruct, wbplayerstruct_t, sitems);
DEFINE_FIELD_X(WBPlayerStruct, wbplayerstruct_t, ssecret);
DEFINE_FIELD_X(WBPlayerStruct, wbplayerstruct_t, stime);
DEFINE_FIELD_X(WBPlayerStruct, wbplayerstruct_t, frags);
DEFINE_FIELD_X(WBPlayerStruct, wbplayerstruct_t, fragcount);

DEFINE_FIELD_X(WBStartStruct, wbstartstruct_t, finished_ep);
DEFINE_FIELD_X(WBStartStruct, wbstartstruct_t, next_ep);
DEFINE_FIELD_X(WBStartStruct, wbstartstruct_t, current);
DEFINE_FIELD_X(WBStartStruct, wbstartstruct_t, next);
DEFINE_FIELD_X(WBStartStruct, wbstartstruct_t, nextname);
DEFINE_FIELD_X(WBStartStruct, wbstartstruct_t, thisname);
DEFINE_FIELD_X(WBStartStruct, wbstartstruct_t, nextauthor);
DEFINE_FIELD_X(WBStartStruct, wbstartstruct_t, thisauthor);
DEFINE_FIELD_X(WBStartStruct, wbstartstruct_t, LName0);
DEFINE_FIELD_X(WBStartStruct, wbstartstruct_t, LName1);
DEFINE_FIELD_X(WBStartStruct, wbstartstruct_t, totalkills);
DEFINE_FIELD_X(WBStartStruct, wbstartstruct_t, maxkills);
DEFINE_FIELD_X(WBStartStruct, wbstartstruct_t, maxitems);
DEFINE_FIELD_X(WBStartStruct, wbstartstruct_t, maxsecret);
DEFINE_FIELD_X(WBStartStruct, wbstartstruct_t, maxfrags);
DEFINE_FIELD_X(WBStartStruct, wbstartstruct_t, partime);
DEFINE_FIELD_X(WBStartStruct, wbstartstruct_t, sucktime);
DEFINE_FIELD_X(WBStartStruct, wbstartstruct_t, totaltime);
DEFINE_FIELD_X(WBStartStruct, wbstartstruct_t, pnum);
DEFINE_FIELD_X(WBStartStruct, wbstartstruct_t, plyr);