/*
	server/entities/mystery_box.qc

	Mystery Box Entity Logic

	Copyright (C) 2021-2024 NZ:P Team

	This program is free software; you can redistribute it and/or
	modify it under the terms of the GNU General Public License
	as published by the Free Software Foundation; either version 2
	of the License, or (at your option) any later version.

	This program is distributed in the hope that it will be useful,
	but WITHOUT ANY WARRANTY; without even the implied warranty of
	MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

	See the GNU General Public License for more details.

	You should have received a copy of the GNU General Public License
	along with this program; if not, write to:

		Free Software Foundation, Inc.
		59 Temple Place - Suite 330
		Boston, MA  02111-1307, USA

*/

void(vector where, float time_alive) SpawnSpark;
void() MBOX_Touch;

#define MBOX_SPAWNFLAG_NOTHERE 		1
#define MBOX_SPAWNFLAG_NOLIGHT 		2

//
// MBOX_UpdateGlowFrame()
// Updates the Glow model frame to match
// the box if it exists.
//
void() MBOX_UpdateGlowFrame =
{
	if (self.goaldummy) self.goaldummy.frame = self.frame;
};

//
// MBOX_PlayOpenAnimation()
// Plays the open animation for the Mystery Box,
// frames 1-8.
//
void() MBOX_PlayOpenAnimation  = [1, MBOX_OpenAnimation2 ] { self.frame = 1; MBOX_UpdateGlowFrame(); Light_Yellow(self); };
void() MBOX_OpenAnimation2 = [2, MBOX_OpenAnimation3 ] { self.frame = 2; MBOX_UpdateGlowFrame(); };
void() MBOX_OpenAnimation3 = [3, MBOX_OpenAnimation4 ] { self.frame = 3; MBOX_UpdateGlowFrame(); };
void() MBOX_OpenAnimation4 = [4, MBOX_OpenAnimation5 ] { self.frame = 4; MBOX_UpdateGlowFrame(); };
void() MBOX_OpenAnimation5 = [5, MBOX_OpenAnimation6 ] { self.frame = 5; MBOX_UpdateGlowFrame(); };
void() MBOX_OpenAnimation6 = [6, MBOX_OpenAnimation7 ] { self.frame = 6; MBOX_UpdateGlowFrame(); };
void() MBOX_OpenAnimation7 = [7, MBOX_OpenAnimation8 ] { self.frame = 7; MBOX_UpdateGlowFrame(); };
void() MBOX_OpenAnimation8 = [8, SUB_Null            ] { self.frame = 8; MBOX_UpdateGlowFrame(); };

//
// MBOX_WaitEnd()
// Ends the per-use delay for the Mystery Box.
//
void() MBOX_WaitEnd =
{
	self.boxstatus = 0;
	self.think = SUB_Null;
}

//
// MBOX_Reset()
// Resets the Mystery Box to be ready for
// another use.
//
inline void() MBOX_Reset = 
{
	self.frame = 0;
	self.boxstatus = -1;

	// Add a 3 second delay before the Mystery Box
	// can be interact with again.
	self.think = MBOX_WaitEnd;
	self.nextthink = time + 3;
};

//
// MBOX_PlayCloseAnimation()
// Plays the open animation for the Mystery Box,
// frames 9-12.
//
void() MBOX_PlayCloseAnimation  = [9, MBOX_CloseAnimation2 ] { self.frame = 9; MBOX_UpdateGlowFrame(); Sound_PlaySound(self, mystery_box_close_sound, SOUND_TYPE_ENV_MUSIC, SOUND_PRIORITY_PLAYALWAYS); Light_None(self); };
void() MBOX_CloseAnimation2 = [10, MBOX_CloseAnimation3 ] { self.frame = 10; MBOX_UpdateGlowFrame(); };
void() MBOX_CloseAnimation3 = [10, MBOX_CloseAnimation4 ] { self.frame = 11; MBOX_UpdateGlowFrame(); };
void() MBOX_CloseAnimation4 = [10, MBOX_Reset           ] { self.frame = 12; MBOX_UpdateGlowFrame(); };

//
// MBOX_FreeEnt(ent)
// Marks an MBOX entity as able to be used.
//
void(entity ent) MBOX_FreeEnt =
{
	setmodel(ent, "");
	ent.classname = "freeMboxEntity";
	ent.touch = SUB_Null;
	ent.think = SUB_Null;
	ent.scale = 1;
    ent.effects = 0;
	ent.frame = 0;

#ifdef FTE

	ent.alpha = 1;

#endif // FTE

};

//
// MBOX_GetFreeEnt()
// Returns an MBOX entity to use.
//
entity() MBOX_GetFreeEnt =
{
	entity ent;
	ent = find(world, classname, "freeMboxEntity");

	if (ent == world)
		error("MBOX_GetFreeEnt: No free MBOX Entity. (Hacks?)\n");
	
	return ent;
};

//
// MBOX_GetRandomBoxWeapon(user)
// Returns a weapon from the Mystery Box allow-list
// that the user is not holding.
//
float(entity user) MBOX_GetRandomBoxWeapon =
{
    float weapon_index = rint((random() * (MAX_BOX_WEAPONS - 1)));
    float weapon_id = mystery_box_weapons[weapon_index].weapon_id;
    float weapon_allowed = mystery_box_weapons[weapon_index].allowed && 
							!mystery_box_weapons[weapon_index].already_obtained;

    if (weapon_allowed == true && !Weapon_PlayerHasWeapon(user, weapon_id, true))
        return weapon_id;
    else
        return MBOX_GetRandomBoxWeapon(user);
};

//
// Reset_MBox()
// Resets the Mystery Box to it's inital State.
//
void() Reset_MBox =
{
	entity tempe;
	self.velocity = '0 0 0';
	tempe = self;
	self = self.owner;
	MBOX_PlayCloseAnimation();
	self = tempe;
	self.owner.owner = world;
	self.owner.boxstatus = 0;
	MBOX_FreeEnt(self);
}

//
// Float_Decreate()
// Make the Gun in the Box slowly descend, eventually
// resetting the Box.
//
void() Float_Decrease =
{
	makevectors(self.angles);
	self.velocity = v_up*-5;
	self.nextthink = time + 7;
	self.think = Reset_MBox;
}

//
// findboxspot()
// Locate a new MBox spot and turn this spot into
// a tp_spot.
//
void() findboxspot =
{	
	local entity newspot;
	local float box = rint(random(mystery_box_count));
	newspot = mystery_boxes[box];

	// Ensure the spot we choose is valid.
	while(newspot == world || newspot == self.owner) {
		box = rint(random(mystery_box_count));
		newspot = mystery_boxes[box];
	}

	// Make our current spot a tp_spot
	self.owner.model = "models/props/teddy.mdl";
	self.owner.frame = 2;
	setmodel(self.owner, self.owner.model);
	self.owner.classname = "mystery_box_tp_spot";
	self.owner.touch = SUB_Null;
	self.owner.angles_y -= 90;
	Light_None(self.owner);
	
	newspot.angles_y += 90;

	// Spawn the Box Glow if permitted
	if (!(self.owner.spawnflags & MBOX_SPAWNFLAG_NOLIGHT))
	{
		entity g;
		g = MBOX_GetFreeEnt();
		g.classname = "mystery_glow";
		newspot.goaldummy = g;
		setmodel(g, mystery_box_glow_model);
		setorigin(g,newspot.origin);
		g.angles = newspot.angles;
		g.effects = EF_FULLBRIGHT;

#ifdef FTE

		g.alpha = 0.5;

#endif // FTE

	}
	
	// Remove teddy
	MBOX_FreeEnt(self);
	
	// Set some values and change the found Spot to an MBox
	newspot.spins = 0;
	newspot.boxstatus = 0;
	newspot.touch = MBOX_Touch;
	newspot.solid=SOLID_TRIGGER;
	newspot.classname = "mystery";
	newspot.spawnflags = self.owner.spawnflags;
	setorigin(newspot, newspot.origin);
	setmodel (newspot, mystery_box_model);
	newspot.frame = 0;
	setsize (newspot, VEC_HULL2_MIN, VEC_HULL2_MAX);

	// Scale down for NZ:P Beta
	if (map_compatibility_mode == MAP_COMPAT_BETA)
		newspot.scale = 0.75;
}

//
// MBOX_MakeActive()
// Sets the Mystery Box touch to MBOX_Touch (delay)
//
void() MBOX_MakeActive =
{
	self.touch = MBOX_Touch;
}

//
// MBOX_FindNewSpot()
// Locate a new location for the Mystery Box.
//
void() MBOX_FindNewSpot =
{
	entity new_box = world;

	// Find a new Mystery Box location
	while(new_box == world || new_box == self) {
		float box_index = rint(random(mystery_box_count));
		new_box = mystery_boxes[box_index];
	}

	// Stupid Teddy Bear being rotated!!
	new_box.angles_y += 90;

	// Convert the spawn to a Mystery Box
	new_box.spawnflags = self.spawnflags;
	new_box.spins = new_box.boxstatus = 0;
	new_box.think = MBOX_MakeActive;
	new_box.nextthink = time + 2;
	new_box.solid = SOLID_TRIGGER;
	new_box.classname = "mystery";
	new_box.frame = 0;
	setmodel(new_box, mystery_box_model);
	setsize(new_box, VEC_HULL2_MIN, VEC_HULL2_MAX);

	// Scale down for NZ:P Beta
	if (map_compatibility_mode == MAP_COMPAT_BETA)
		new_box.scale = 0.75;

	// Spawn the Box Glow if permitted
	if (!(new_box.spawnflags & MBOX_SPAWNFLAG_NOLIGHT))
	{
		entity light;
		light = MBOX_GetFreeEnt();
		light.classname = "mystery_glow";
		new_box.goaldummy = light;
		setmodel(light, mystery_box_glow_model);
		setorigin(light, new_box.origin);
		light.angles = new_box.angles;
		light.effects = EF_FULLBRIGHT;

#ifdef FTE

		light.alpha = 0.5;

#endif // FTE

	}

	// Spawn some sparkles so it doesn't just "appear"
	SpawnSpark(new_box.origin, 0.75);
	// "Woosh" Effect
	Sound_PlaySound(new_box, "sounds/pu/drop.wav", SOUND_TYPE_ENV_OBJECT, SOUND_PRIORITY_PLAYALWAYS);
}

//
// MBOX_StopFlying()
// We've done our dance, go away and
// delay respawning.
//
void() MBOX_StopFlying = 
{
	// Spark Effect
	SpawnSpark(self.origin, 0.75);
	// "Woosh" Effect
	Sound_PlaySound(self, "sounds/pu/drop.wav", SOUND_TYPE_ENV_OBJECT, SOUND_PRIORITY_PLAYALWAYS);
	// Disappear!
	self.model = "models/props/teddy.mdl";
	setmodel(self, self.model);
	self.frame = 2;
	// Clean up
	self.origin = self.oldvelocity;
	self.angles = self.movement;
	self.velocity = 0;
	self.angles_y -= 90;
	self.classname = "mystery_box_tp_spot";
	self.touch = SUB_Null;
	// 12 Second delay to finding a new spot
	self.think = MBOX_FindNewSpot;
	self.nextthink = time + 12;
}

//
// MBOX_AnimateFly()
// Rotate the Mystery Box as it flies away.
//
void() MBOX_AnimateFly =
{
	if (self.score == 0)
		self.angles_x += 6;
	else
		self.angles_x -= 6;

	if (self.angles_x - self.movement_x >= 20)
		self.score = 1;
	else if (self.angles_x - self.movement_x <= -20)
		self.score = 0;

	if(self.ltime < time) {
		MBOX_StopFlying();
	} else {
		self.nextthink = time + 0.05;
	}
}

//
// MBOX_FlyAway()
// The actual leave animation.
//
void() MBOX_FlyAway =
{
	// Begin rising
	makevectors(self.angles);
	self.movetype = MOVETYPE_NOCLIP;
	self.velocity = v_up * 7;
	// Animation lasts 4 seconds
	self.ltime = time + 4;
	// Animation Update
	self.think = MBOX_AnimateFly;
	self.nextthink = time + 0.05;
}

//
// MBOX_Leave()
// Starts Mystery Box Leave animation.
//
void() MBOX_Leave =
{
	// Broadcast the laughter
	Sound_PlaySound(world, "sounds/pu/byebye.wav", SOUND_TYPE_ENV_VOICE, SOUND_PRIORITY_PLAYALWAYS);
	// "Woosh" Effect
	Sound_PlaySound(self, "sounds/pu/drop.wav", SOUND_TYPE_ENV_OBJECT, SOUND_PRIORITY_PLAYALWAYS);
	// Spark Effect
	SpawnSpark(self.origin, 0.75);
	// Turn off Light & Glow
	Light_None(self);
	if (!(self.spawnflags & MBOX_SPAWNFLAG_NOLIGHT)) 
		if (self.goaldummy != world)
			MBOX_FreeEnt(self.goaldummy);
	// Save old position and angle
	self.oldvelocity = self.origin;
	self.movement = self.angles;
	// Start flying
	self.think = MBOX_FlyAway;
	self.nextthink = time + 0.05;
}

//
// MBOX_TeddyDespawn()
// Frees the Teddy Bear entity.
//
void() MBOX_TeddyDespawn =
{
	self.velocity = 0;
	MBOX_FreeEnt(self);
}

//
// MBOX_TeddyLeave()
// Make the Teddy Bear float away and start the
// timer for the Mystery Box to follow.
//
void() MBOX_TeddyLeave =
{
	// Fly slowly
	self.velocity = v_up*75;
	self.think = MBOX_TeddyDespawn;
	self.nextthink = time + 5;
	// Linger for 3 seconds, and then the Box follows.
	self.owner.think = MBOX_Leave;
	self.owner.nextthink = time + 3;
}

//
// MBOX_PresentTeddy()
// Lets the Teddy Bear idle for a few seconds,
// return player points, kick-off leave.
//
void() MBOX_PresentTeddy =
{
	// Return the Player's points.
	Player_AddScore(self.owner.owner, mystery_box_cost, false);

	// Broadcast the bad luck.
	Sound_PlaySound(self, "sounds/misc/buy.wav", SOUND_TYPE_ENV_OBJECT, SOUND_PRIORITY_PLAYALWAYS);
	Sound_PlaySound(self, "sounds/misc/giggle.wav", SOUND_TYPE_ENV_VOICE, SOUND_PRIORITY_PLAYALWAYS);

	// Linger for 2 seconds, and then fly away.
	self.think = MBOX_TeddyLeave;
	self.nextthink = time + 2;
}

//
// MBOX_GetLeaveChancePercentage()
// Generates a percentage chance for the Mystery Box
// to leave.
//
float(float uses) MBOX_GetLeaveChancePercentage =
{
    // If there's only one Mystery Box, never try to leave.
    // Additionally, you're always given 4 free uses.
    if (mystery_box_count == 1 || uses <= 4)
        return 0;

    float chance_to_leave = 0;
    
    // There is a hardcoded 100% chance to leave after 8 uses
    // if the box hasn't moved yet in a game.
    if (mystery_box_leave_count == 0 && uses >= 8)
        chance_to_leave = 100;

    // There's a 15% chance for leaving if this is between the uses
    // of 4 and 8.
    if (uses >= 4 && uses < 8)
        chance_to_leave = 15;

    // Mystery Box behavior changes after it leaves for the first time
    if (mystery_box_leave_count > 0) {
        // 30% chance of leaving between uses 8 and 12.
        if (uses >= 8 && uses < 12)
            chance_to_leave = 30;
        // 50% chance after the 12th use.
        else if (uses >= 12)
            chance_to_leave = 50;
    }

    return chance_to_leave/100;
}

void() Float_Change =
{
	float tempf;
	string temps;

    float leave_chance;

	tempf = MBOX_GetRandomBoxWeapon(self.owner.owner);
	temps = GetWeaponModel(tempf, 1);
	setmodel (self, temps);

	self.boxstatus = self.boxstatus + 0.01;
	if (self.wait <= time)
	{
        leave_chance = MBOX_GetLeaveChancePercentage(self.owner.spins);
				
		self.velocity = '0 0 0';
		
		if (random() > leave_chance) {		//teddy gen threshold, high means less chance
			self.owner.boxstatus = 2;
			self.weapon = tempf;
			self.nextthink = time + 5;
			self.think = Float_Decrease;
			//bprint(PRINT_HIGH, "spot not found, or teddygun is > 0.7\n");
			return;
		}
		else {
			self.model = "models/props/teddy.mdl";
			setmodel(self, self.model);
			self.angles_y = self.angles_y - 90;
			self.nextthink = time + 1;
			self.think = MBOX_PresentTeddy;
			mystery_box_leave_count++;

			if (mystery_box_leave_count >= 10)
				GiveAchievement(11);
			return;
		}
	}
	if (self.ltime <= time)
	{
		self.velocity_z = self.velocity_z*0.5;
		self.ltime = 0.5;
	}
	self.nextthink = time + self.boxstatus;
	self.think = Float_Change;
}

void() finish_mbox_setup =
{
	// Temporary hack for random box spawns until rewrite - Mikey (27/03/2023, DD/MM/YYYY)
    if(self.spawnflags & MBOX_SPAWNFLAG_NOTHERE) {
        entity temp = MBOX_GetFreeEnt();
		temp.classname = "mystery_helper";

        temp.owner = self;
        temp.think = findboxspot;
        temp.nextthink = time + 0.1;
    } else if(!(self.spawnflags & MBOX_SPAWNFLAG_NOLIGHT)) {
        entity g = MBOX_GetFreeEnt();
		g.classname = "mystery_glow";

		self.goaldummy = g;
		setmodel(g, mystery_box_glow_model);
		setorigin(g,self.origin);
		g.angles = self.angles;
		g.effects = EF_FULLBRIGHT;

#ifdef FTE

		g.alpha = 0.5;

#endif // FTE

    }
}

//
// MBOX_AllocateTempEntities()
// Spawns in all of the temp entities that are
// used for the floating weapons, teddy bear,
// and glow.
//
void() MBOX_AllocateTempEntities =
{
	self.think = SUB_Null;

	for (float i = 0; i < mystery_box_count * 3; i++) {
		entity tempe = spawn();
		tempe.classname = "freeMboxEntity";
	}

	finish_mbox_setup();
};

void() Create_Floating_Weapon =
{
	entity gun;
	float tempf;
	string temps;

	tempf = MBOX_GetRandomBoxWeapon(self.owner.owner);
	temps = GetWeaponModel(tempf, 1);
		
	gun = MBOX_GetFreeEnt();
	gun.classname = "mystery_weapon";
	
	setorigin (gun, self.origin);
	setmodel (gun, temps);
	setsize (gun, '0 0 0', '0 0 0');
	gun.angles = self.angles;
	
	gun.movetype = MOVETYPE_NOCLIP;
	gun.solid = SOLID_NOT;
	makevectors(self.angles);

	// Float up less in NZ:P Beta, and scale down.
	if (map_compatibility_mode == MAP_COMPAT_BETA) {
		gun.velocity = v_up*10;
		gun.scale = 0.75;
	} else {
		gun.velocity = v_up*15;
	}
	
	gun.owner = self;
	self.boxweapon = gun;
	
	gun.ltime = time+2;
	gun.boxstatus = 0.01;
	gun.wait = time + 5;
	gun.nextthink = time + 0.01;
	gun.think = Float_Change;
}

void() mystery_box_tp_spot = 
{
	precache_model ("models/props/teddy.mdl"); 
	precache_sound("sounds/pu/byebye.wav");
	precache_sound("sounds/misc/giggle.wav");
	
	self.solid=SOLID_TRIGGER;
	self.classname = "mystery_box_tp_spot";
	setorigin(self, self.origin);
	setmodel (self, "models/props/teddy.mdl");
	setsize (self, VEC_HULL2_MIN, VEC_HULL2_MAX);
	
	if (self.model == "models/props/teddy.mdl") {
		self.frame = 2;
		self.angles_y -= 90;
	}
	
	mystery_boxes[mystery_box_count] = self;
	mystery_box_count++;
};

void() MBOX_UpdatePosessionStatus =
{
	// Set all weapon statuses as unobtained.
	for(float i = 0; i < MAX_BOX_WEAPONS; i++) {
		mystery_box_weapons[i].already_obtained = false;
	}

	entity player = find(world, classname, "player");

	while(player != world) {
		for(float i = 0; i < MAX_BOX_WEAPONS; i++) {
			float weapon_id = mystery_box_weapons[i].weapon_id;
			if (Weapon_PlayerHasWeapon(player, weapon_id, true) && WepDef_OnlyOneAllowed(weapon_id)) {
				mystery_box_weapons[i].already_obtained = true;
			}
		}
		player = find(player, classname, "player");
	}
}

void() MBOX_Touch =
{
	entity tempe;
	
	if (other.classname != "player" || other.downed)
		return;
		
	if (!self.boxstatus) {
		useprint (other, 6, mystery_box_cost, 0);
	}
	if (self.boxstatus == 2 && self.owner == other) {

#ifndef FTE

		other.Weapon_Name_Touch = GetWeaponName(self.boxweapon.weapon);

#endif // FTE

		useprint (other, 7, 0, self.boxweapon.weapon);
	}
	
	if (other.button7 && !(other.semi_actions & SEMIACTION_USE))
	{
		other.semi_actions |= SEMIACTION_USE;
		if (!self.boxstatus)
		{
			if (other.points >= mystery_box_cost)
			{					
				Sound_PlaySound(self, mystery_box_open_sound, SOUND_TYPE_ENV_MUSIC, SOUND_PRIORITY_PLAYALWAYS);
				Player_RemoveScore(other, mystery_box_cost);
				self.boxstatus = 1;
				self.owner = other;
				MBOX_PlayOpenAnimation();
				Create_Floating_Weapon();
				MBOX_UpdatePosessionStatus();
				self.spins++;
			}
			else {
        		centerprint (other, STR_NOTENOUGHPOINTS);
				Sound_PlaySound(other, "sounds/misc/denybuy.wav", SOUND_TYPE_ENV_CHING, SOUND_PRIORITY_PLAYALWAYS);
			}
		}
		if (self.boxstatus == 2)
		{
			if (self.owner == other)
			{
				other.reload_delay = 0;
				self.owner = world;
				Sound_PlaySound(self, "sounds/misc/ching.wav", SOUND_TYPE_ENV_CHING, SOUND_PRIORITY_PLAYALWAYS);
				tempe = self;
				self = other;

				Weapon_GiveWeapon(tempe.boxweapon.weapon, 0, 0);
				self = tempe;
				MBOX_FreeEnt(self.boxweapon);
				MBOX_PlayCloseAnimation();
			}
		}
	}
}

//
// MBOX_GetWeaponIDFromMB1(mbox_id)
// .mbox weapon IDs did not 100% correlate
// to internal weapon IDs, so this converts
// them appropriately.
//
float MBOX_GetWeaponIDFromMB1(float mbox_id) =
{
    switch(mbox_id) {
        case 0: return W_COLT;
        case 1: return W_KAR;
        case 2: return W_DB;
        case 3: return W_MG;
        case 4: return W_RAY;
        case 5: return W_THOMPSON;
        case 6: return W_M2;
        case 7: return W_PPSH;
        case 8: return W_SAWNOFF;
        case 9: return W_TESLA;
        case 10: return W_M1A1;
        case 11: return W_GEWEHR;
        case 12: return W_FG;
        case 13: return W_BROWNING;
        case 14: return W_KAR_SCOPE;
        case 15: return W_357;
        case 16: return W_STG;
        case 17: return W_PANZER;
        case 18: return W_BK;
        case 19: return W_PTRS;
        case 20: return W_MP40;
        case 21: return W_TRENCH;
        case 22: return W_BAR;
        case 23: return W_M1;
        case 24: return W_TYPE;
        case 25: return W_MP5K;
        case 26: return W_SPRING;
        default: return W_COLT;
    }
    return W_COLT;
}

//
// MBOX_PrecacheWeaponData()
// Iterates through the weapon array and
// allocates necessary content for each
// allowed weapon.
//
void() MBOX_PrecacheWeaponContent =
{
    for (float i = 0; i < MAX_BOX_WEAPONS; i++) {
        if (mystery_box_weapons[i].allowed == true) {
            precache_model(GetWeaponModel(mystery_box_weapons[i].weapon_id, 0));
		    precache_model(GetWeaponModel(mystery_box_weapons[i].weapon_id, 1));
		    precache_extra(mystery_box_weapons[i].weapon_id);
        }
    }
}

//
// MBOX_ParseMB1File(mbox_file)
// Parses the old (2014) .mbox file and loads
// it into the necessary structure.
//
void(float mbox_file) MBOX_ParseMB1File =
{
    string  file_line;

    // Parse the .mbox line-by-line and fill the data structure.
    for (float i = 0; i < MAX_BOX_WEAPONS; i++) {
        file_line = strtrim((fgets(mbox_file)));
        mystery_box_weapons[i].weapon_id = MBOX_GetWeaponIDFromMB1(i);
        mystery_box_weapons[i].allowed = stof(file_line);
        mystery_box_weapons[i].rarity = -1;
    }

    // Precache the allowed weapons
    MBOX_PrecacheWeaponContent();
}

//
// MBOX_GetUnusedWeaponID()
// Returns a weapon ID for a potential
// box weapon that is not currently
// in the list.
//
float() MBOX_GetUnusedWeaponID =
{
    // Iterate over every potential Box Weapon
    for (float i = 0; i < MAX_BOX_WEAPONS; i++) {
        float weapon_in_use = false;
        float weapon_id = MBOX_GetWeaponIDFromMB1(i);

        // Now over every weapon in the list
        for (float j = 0; j < MAX_BOX_WEAPONS; j++) {
            if (mystery_box_weapons[j].weapon_id == weapon_id)
                weapon_in_use = true;
        }

        if (weapon_in_use == false) {
            return weapon_id;
        }
    }

    error("MBOX_GetunusedWeaponID: Could not find a free weapon slot!\n");
    return 0;
}

//
// MBOX_ParseMB2File(mbox_file)
// Parses the current .mb2 file and loads it
// into the necessary structure.
//
void(float mbox_file) MBOX_ParseMB2File =
{
    string file_line;

    // Read the first line: It's our "header", should be
    // "nzp_mbox2".
    file_line = strtrim((fgets(mbox_file)));

    if (file_line != "nzp_mbox2")
        error(strcat("MBOX_ParseMB2File: Expected \"nzp_mbox2\" but got ", file_line));

    // Read each line of the file in a loop
    float finished_parsing = false;
    float parsing_state = 0;
    float is_allow_not_deny = false;
    float weapons_parsed = 0;
    float weapon_id = 0;
    while(!finished_parsing) {
        // Grab a new line
        file_line = fgets(mbox_file);

        // End of file.
        if not (file_line) {
            finished_parsing = true;
            break;
        }

        file_line = strzone(strtrim(file_line));

        // Check for comments, they always start with '#'.
        // Also ignore whitespace.
        string first_char = strzone(substring(file_line, 0, 1));
        if (first_char == "#" || file_line == "") {
            continue;
        }
        strunzone(first_char);

        //
        // Actual Content Parsing
        //

        // Retrieving if its an allow or deny list.
        if (parsing_state == 0) {
            if (file_line == "allow:")
                is_allow_not_deny = true;
            else if (file_line == "deny:")
                is_allow_not_deny = false;
            else
                error(strcat("MBOX_ParseMB2File: Not an allow/deny specifier - ", file_line));

            // Now that we know the list, we can begin
            // parsing the weapons inside of it.
            parsing_state++;
        } else if (parsing_state == 1) {
            // Get the weapon ID from it's name.
            weapon_id = WepDef_GetWeaponIDFromName(file_line);

            if (weapon_id == W_NOWEP)
                error(strcat("MBOX_ParseMB2File: Not a weapon - ", file_line));

            // Fill the list with it.
            mystery_box_weapons[weapons_parsed].weapon_id = weapon_id;
            mystery_box_weapons[weapons_parsed].allowed = is_allow_not_deny;
            weapons_parsed++;
        }

        strunzone(file_line);
    }

    // At this point, the file has been parsed with all of the
    // allowed/denied weapons, now we have to automatically
    // fill in the rest for the opposite list.
    for (float i = weapons_parsed; i < MAX_BOX_WEAPONS; i++) {
        // We need to pick an ID to assign that hasn't already
        // been used.
        weapon_id = MBOX_GetUnusedWeaponID();
        mystery_box_weapons[i].weapon_id = weapon_id;
        mystery_box_weapons[i].allowed = !is_allow_not_deny;
    }

    // Precache the allowed weapons
    MBOX_PrecacheWeaponContent();
}

//
// MBOX_LoadData()
// Seeks for either an .mb2 or .mbox file,
// and adds the contents into the Mystery Box
// weapon list.
//
void() MBOX_LoadData =
{
    float mbox_file;
    string file_path;

    // Attempt 1: Seek for maps/mapname.mb2 (mbox-2)
    file_path = strcat(mapname, ".mb2");
    file_path = strcat("maps/", file_path);
    mbox_file = fopen(file_path, FILE_READ);
    if (mbox_file != -1) {
        MBOX_ParseMB2File(mbox_file);
        fclose(mbox_file);
        return;
    }

    // Attempt 2: Seek for maps/mapname.mbox (mbox-1)
    file_path = strcat(mapname, ".mbox");
    file_path = strcat("maps/", file_path);
    mbox_file = fopen(file_path, FILE_READ);
    if (mbox_file != -1) {
        MBOX_ParseMB1File(mbox_file);
        fclose(mbox_file);
        return;
    }

    // Attempt 3: Include All Weapons
    for (float i = 0; i < MAX_BOX_WEAPONS; i++) {
        mystery_box_weapons[i].weapon_id = MBOX_GetWeaponIDFromMB1(i);
        mystery_box_weapons[i].allowed = true;
        mystery_box_weapons[i].rarity = -1;
    }

    // Precache it all.
    MBOX_PrecacheWeaponContent();
    
    // Close the file pointer just in case.
    fclose(mbox_file);
}

void() mystery_box =
{
    // Load and cache all of the allowed weapons into the hunk.
    MBOX_LoadData();

    //
	// Set Default Stats for Compatibility
	//
	
	// Model
	if (!self.model) {
		self.model = "models/machines/mystery.mdl";
	}

    // Light Model
	if (!self.weapon2model) {
		self.weapon2model = "models/machines/mglow$.mdl";
	}

    // Cost
	if (!self.cost) {
		self.cost = 950;
	}

    // Open Sound
	if (!self.oldmodel) {
		self.oldmodel = "sounds/machines/mbox_open.wav";
	}

	// Close Sound
	if (!self.powerup_vo) {
		self.powerup_vo = "sounds/machines/mbox_close.wav";
	}

    // Store the custom presentation as globals
    mystery_box_model = self.model;
    mystery_box_glow_model = self.weapon2model;
    mystery_box_open_sound = self.oldmodel;
    mystery_box_close_sound = self.powerup_vo;
    mystery_box_cost = self.cost;

	precache_model(mystery_box_model);

    if (!(self.spawnflags & MBOX_SPAWNFLAG_NOLIGHT))
		precache_model (mystery_box_glow_model);

	precache_sound(mystery_box_open_sound); 
	precache_sound(mystery_box_close_sound); 
	
	self.solid = SOLID_TRIGGER;
	self.classname = "mystery";
	setorigin(self, self.origin);
	setmodel (self, mystery_box_model);
	setsize (self, VEC_HULL2_MIN, VEC_HULL2_MAX);

	self.touch = MBOX_Touch;

	mystery_box_start_origin = self.origin;
	mystery_boxes[mystery_box_count] = self;
	mystery_box_count++;

	self.think = MBOX_AllocateTempEntities;
	self.nextthink = time + 0.2;
};