mirror of
https://github.com/ZDoom/gzdoom-last-svn.git
synced 2025-05-30 17:00:48 +00:00
* Updated to ZDoom r3360:
- Added ability to use a constant for the maximum comparator for health and armor drawbars. - Added support for loading named ACS scripts. You can't run them directly at the moment, but you can still use them for automatically executed script types (like open and enter). - Change the DACSThinker::RunningScripts array into a TMap so that it can catalog the new range of ACS scripts (up to 32767). - Removed snd_3dspread, because it totally does not do what I want. I was using it to preserve some of the stereoness of stereo sounds played in 3D. My testing was done only with stereo speakers, however, and I did not realize that it was moving the perceived physical location of the sound itself (because it sounded fine with my two speakers). So when 3D spread started working with mono sounds as well in FMOD 4.28, sound positioning was completely broken for everything when outputting to more than two speakers, because sounds were being spread across a 180 degree arc. Whoops! Stereo sounds are now completely mono when not played by you, the listener. - Added keys on the automap for Heretic in easy mode. - Fixed: Sound limiting applied even to sounds that were already playing on the same actor+channel. Since in this case, it's really just restarting the sound, it shouldn't limit it. (Since it's already playing, we know the limit wasn't exceeded when it started playing, so it shouldn't be exceeded if we restart it now.) - Fixed: C_DoKey() must disable all doublebind processing if it isn't passed any doublebindings. This is because the automap calls it with its own bindings, which effectively cancelled all doublebindings while the automap was open. - Fixed: CheckMobjBlocking() did not consider one-sided lines without the ML_BLOCKING flag to be blocking. - Fixed: Forgot to divide the length of the SVCT chunks in ACS objects by 4 to get the actual number of scripts to set VarCount for. - Fixed: DCanvas::DrawTextV needs to accept the entire tag list in one parameter. Otherwise, it can't pass it to DrawTexture with a simple TAG_MORE. (Not sure why I thought the initial tag needed to be separate, though it did catch one case where it wasn't provided.) git-svn-id: http://mancubus.net/svn/hosted/gzdoom/trunk@1284 b0f79afe-0144-0410-b225-9a4edf0717df
This commit is contained in:
parent
e6e6f45a91
commit
d95f3ca348
15 changed files with 329 additions and 133 deletions
125
src/am_map.cpp
125
src/am_map.cpp
|
@ -308,6 +308,7 @@ struct islope_t
|
|||
static TArray<mline_t> MapArrow;
|
||||
static TArray<mline_t> CheatMapArrow;
|
||||
static TArray<mline_t> CheatKey;
|
||||
static TArray<mline_t> EasyKey;
|
||||
|
||||
#define R (MAPUNIT)
|
||||
// [RH] Avoid lots of warnings without compiler-specific #pragmas
|
||||
|
@ -536,10 +537,12 @@ void AM_StaticInit()
|
|||
MapArrow.Clear();
|
||||
CheatMapArrow.Clear();
|
||||
CheatKey.Clear();
|
||||
EasyKey.Clear();
|
||||
|
||||
if (gameinfo.mMapArrow.IsNotEmpty()) AM_ParseArrow(MapArrow, gameinfo.mMapArrow);
|
||||
if (gameinfo.mCheatMapArrow.IsNotEmpty()) AM_ParseArrow(CheatMapArrow, gameinfo.mCheatMapArrow);
|
||||
AM_ParseArrow(CheatKey, "maparrows/key.txt");
|
||||
AM_ParseArrow(EasyKey, "maparrows/ravenkey.txt");
|
||||
if (MapArrow.Size() == 0) I_FatalError("No automap arrow defined");
|
||||
|
||||
char namebuf[9];
|
||||
|
@ -1917,13 +1920,13 @@ bool AM_Check3DFloors(line_t *line)
|
|||
//=============================================================================
|
||||
|
||||
void AM_drawWalls (bool allmap)
|
||||
{
|
||||
int i;
|
||||
static mline_t l;
|
||||
int lock, color;
|
||||
|
||||
for (i = 0; i < numlines; i++)
|
||||
{
|
||||
{
|
||||
int i;
|
||||
static mline_t l;
|
||||
int lock, color;
|
||||
|
||||
for (i = 0; i < numlines; i++)
|
||||
{
|
||||
l.a.x = lines[i].v1->x >> FRACTOMAPBITS;
|
||||
l.a.y = lines[i].v1->y >> FRACTOMAPBITS;
|
||||
l.b.x = lines[i].v2->x >> FRACTOMAPBITS;
|
||||
|
@ -1953,22 +1956,22 @@ void AM_drawWalls (bool allmap)
|
|||
else if (lines[i].flags & ML_SECRET)
|
||||
{ // secret door
|
||||
if (am_cheat != 0 && lines[i].backsector != NULL)
|
||||
AM_drawMline(&l, SecretWallColor);
|
||||
else
|
||||
AM_drawMline(&l, WallColor);
|
||||
} else if (lines[i].locknumber > 0) { // [Dusk] specials w/ locknumbers
|
||||
lock = lines[i].locknumber;
|
||||
color = P_GetMapColorForLock(lock);
|
||||
|
||||
AMColor c;
|
||||
if (color >= 0) c.FromRGB(RPART(color), GPART(color), BPART(color));
|
||||
else c = LockedColor;
|
||||
|
||||
AM_drawMline (&l, c);
|
||||
} else if ((lines[i].special == Teleport ||
|
||||
lines[i].special == Teleport_NoFog ||
|
||||
lines[i].special == Teleport_ZombieChanger ||
|
||||
lines[i].special == Teleport_Line) &&
|
||||
AM_drawMline(&l, SecretWallColor);
|
||||
else
|
||||
AM_drawMline(&l, WallColor);
|
||||
} else if (lines[i].locknumber > 0) { // [Dusk] specials w/ locknumbers
|
||||
lock = lines[i].locknumber;
|
||||
color = P_GetMapColorForLock(lock);
|
||||
|
||||
AMColor c;
|
||||
if (color >= 0) c.FromRGB(RPART(color), GPART(color), BPART(color));
|
||||
else c = LockedColor;
|
||||
|
||||
AM_drawMline (&l, c);
|
||||
} else if ((lines[i].special == Teleport ||
|
||||
lines[i].special == Teleport_NoFog ||
|
||||
lines[i].special == Teleport_ZombieChanger ||
|
||||
lines[i].special == Teleport_Line) &&
|
||||
(lines[i].activation & SPAC_PlayerActivate) &&
|
||||
am_colorset == 0)
|
||||
{ // intra-level teleporters
|
||||
|
@ -1988,18 +1991,18 @@ void AM_drawWalls (bool allmap)
|
|||
(lines[i].special == Door_Animated && lines[i].args[3] != 0) ||
|
||||
(lines[i].special == Generic_Door && lines[i].args[4] != 0))
|
||||
{
|
||||
if (am_colorset == 0 || am_colorset == 3) // Raven games show door colors
|
||||
{
|
||||
int P_GetMapColorForLock(int lock);
|
||||
|
||||
if (lines[i].special==Door_LockedRaise || lines[i].special==Door_Animated)
|
||||
lock=lines[i].args[3];
|
||||
else lock=lines[i].args[4];
|
||||
|
||||
color = P_GetMapColorForLock(lock);
|
||||
|
||||
AMColor c;
|
||||
|
||||
if (am_colorset == 0 || am_colorset == 3) // Raven games show door colors
|
||||
{
|
||||
int P_GetMapColorForLock(int lock);
|
||||
|
||||
if (lines[i].special==Door_LockedRaise || lines[i].special==Door_Animated)
|
||||
lock=lines[i].args[3];
|
||||
else lock=lines[i].args[4];
|
||||
|
||||
color = P_GetMapColorForLock(lock);
|
||||
|
||||
AMColor c;
|
||||
|
||||
if (color >= 0) c.FromRGB(RPART(color), GPART(color), BPART(color));
|
||||
else c = LockedColor;
|
||||
|
||||
|
@ -2261,6 +2264,49 @@ void AM_drawPlayers ()
|
|||
//
|
||||
//=============================================================================
|
||||
|
||||
void AM_drawKeys ()
|
||||
{
|
||||
AMColor color;
|
||||
mpoint_t p;
|
||||
angle_t angle;
|
||||
|
||||
TThinkerIterator<AKey> it;
|
||||
AKey *key;
|
||||
|
||||
while ((key = it.Next()) != NULL)
|
||||
{
|
||||
p.x = key->x >> FRACTOMAPBITS;
|
||||
p.y = key->y >> FRACTOMAPBITS;
|
||||
angle = key->angle;
|
||||
|
||||
if (am_rotate == 1 || (am_rotate == 2 && viewactive))
|
||||
{
|
||||
AM_rotatePoint (&p.x, &p.y);
|
||||
angle += ANG90 - players[consoleplayer].camera->angle;
|
||||
}
|
||||
|
||||
color = ThingColor;
|
||||
if (key->flags & MF_SPECIAL)
|
||||
{
|
||||
// Find the key's own color.
|
||||
// Only works correctly if single-key locks have lower numbers than any-key locks.
|
||||
// That is the case for all default keys, however.
|
||||
int P_GetMapColorForKey (AInventory * key);
|
||||
int c = P_GetMapColorForKey(key);
|
||||
|
||||
if (c >= 0) color.FromRGB(RPART(c), GPART(c), BPART(c));
|
||||
else color = ThingColor_CountItem;
|
||||
AM_drawLineCharacter(&EasyKey[0], EasyKey.Size(), 0, 0, color, p.x, p.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//=============================================================================
|
||||
//
|
||||
//
|
||||
//
|
||||
//=============================================================================
|
||||
|
||||
void AM_drawThings ()
|
||||
{
|
||||
AMColor color;
|
||||
|
@ -2299,7 +2345,12 @@ void AM_drawThings ()
|
|||
// That is the case for all default keys, however.
|
||||
if (t->IsKindOf(RUNTIME_CLASS(AKey)))
|
||||
{
|
||||
if (am_showkeys)
|
||||
if (G_SkillProperty(SKILLP_EasyKey))
|
||||
{
|
||||
// Already drawn by AM_drawKeys(), so don't draw again
|
||||
color.Index = -1;
|
||||
}
|
||||
else if (am_showkeys)
|
||||
{
|
||||
int P_GetMapColorForKey (AInventory * key);
|
||||
int c = P_GetMapColorForKey(static_cast<AKey *>(t));
|
||||
|
@ -2521,6 +2572,8 @@ void AM_Drawer ()
|
|||
|
||||
AM_drawWalls(allmap);
|
||||
AM_drawPlayers();
|
||||
if (G_SkillProperty(SKILLP_EasyKey))
|
||||
AM_drawKeys();
|
||||
if (am_cheat >= 2 || allthings)
|
||||
AM_drawThings();
|
||||
|
||||
|
|
|
@ -829,6 +829,7 @@ bool C_DoKey (event_t *ev, FKeyBindings *binds, FKeyBindings *doublebinds)
|
|||
bool dclick;
|
||||
int dclickspot;
|
||||
BYTE dclickmask;
|
||||
unsigned int nowtime;
|
||||
|
||||
if (ev->type != EV_KeyDown && ev->type != EV_KeyUp)
|
||||
return false;
|
||||
|
@ -841,10 +842,11 @@ bool C_DoKey (event_t *ev, FKeyBindings *binds, FKeyBindings *doublebinds)
|
|||
dclick = false;
|
||||
|
||||
// This used level.time which didn't work outside a level.
|
||||
if (DClickTime[ev->data1] > I_MSTime() && ev->type == EV_KeyDown)
|
||||
nowtime = I_MSTime();
|
||||
if (doublebinds != NULL && DClickTime[ev->data1] > nowtime && ev->type == EV_KeyDown)
|
||||
{
|
||||
// Key pressed for a double click
|
||||
if (doublebinds != NULL) binding = doublebinds->GetBinding(ev->data1);
|
||||
binding = doublebinds->GetBinding(ev->data1);
|
||||
DClicked[dclickspot] |= dclickmask;
|
||||
dclick = true;
|
||||
}
|
||||
|
@ -853,11 +855,11 @@ bool C_DoKey (event_t *ev, FKeyBindings *binds, FKeyBindings *doublebinds)
|
|||
if (ev->type == EV_KeyDown)
|
||||
{ // Key pressed for a normal press
|
||||
binding = binds->GetBinding(ev->data1);
|
||||
DClickTime[ev->data1] = I_MSTime() + 571;
|
||||
DClickTime[ev->data1] = nowtime + 571;
|
||||
}
|
||||
else if (DClicked[dclickspot] & dclickmask)
|
||||
else if (doublebinds != NULL && DClicked[dclickspot] & dclickmask)
|
||||
{ // Key released from a double click
|
||||
if (doublebinds != NULL) binding = doublebinds->GetBinding(ev->data1);
|
||||
binding = doublebinds->GetBinding(ev->data1);
|
||||
DClicked[dclickspot] &= ~dclickmask;
|
||||
DClickTime[ev->data1] = 0;
|
||||
dclick = true;
|
||||
|
|
|
@ -545,7 +545,8 @@ enum ESkillProperty
|
|||
SKILLP_MonsterHealth,
|
||||
SKILLP_FriendlyHealth,
|
||||
SKILLP_NoPain,
|
||||
SKILLP_ArmorFactor
|
||||
SKILLP_ArmorFactor,
|
||||
SKILLP_EasyKey,
|
||||
};
|
||||
int G_SkillProperty(ESkillProperty prop);
|
||||
const char * G_SkillName();
|
||||
|
@ -564,6 +565,7 @@ struct FSkillInfo
|
|||
bool AutoUseHealth;
|
||||
|
||||
bool EasyBossBrain;
|
||||
bool EasyKey;
|
||||
int RespawnCounter;
|
||||
int RespawnLimit;
|
||||
fixed_t Aggressiveness;
|
||||
|
|
|
@ -2235,8 +2235,8 @@ class CommandDrawBar : public SBarInfoCommand
|
|||
{
|
||||
public:
|
||||
CommandDrawBar(SBarInfo *script) : SBarInfoCommand(script),
|
||||
border(0), horizontal(false), reverse(false), foreground(-1), background(-1),
|
||||
type(HEALTH), inventoryItem(NULL), interpolationSpeed(0), drawValue(0)
|
||||
border(0), horizontal(false), reverse(false), foreground(-1),
|
||||
background(-1), type(HEALTH), interpolationSpeed(0), drawValue(0)
|
||||
{
|
||||
}
|
||||
|
||||
|
@ -2297,28 +2297,12 @@ class CommandDrawBar : public SBarInfoCommand
|
|||
if(sc.Compare("health"))
|
||||
{
|
||||
type = HEALTH;
|
||||
if(sc.CheckToken(TK_Identifier)) //comparing reference
|
||||
{
|
||||
inventoryItem = PClass::FindClass(sc.String);
|
||||
if(inventoryItem == NULL || !RUNTIME_CLASS(AInventory)->IsAncestorOf(inventoryItem)) //must be a kind of inventory
|
||||
{
|
||||
sc.ScriptMessage("'%s' is not a type of inventory item.", sc.String);
|
||||
inventoryItem = RUNTIME_CLASS(AInventory);
|
||||
}
|
||||
}
|
||||
ParseComparator(sc);
|
||||
}
|
||||
else if(sc.Compare("armor"))
|
||||
{
|
||||
type = ARMOR;
|
||||
if(sc.CheckToken(TK_Identifier))
|
||||
{
|
||||
inventoryItem = PClass::FindClass(sc.String);
|
||||
if(inventoryItem == NULL || !RUNTIME_CLASS(AInventory)->IsAncestorOf(inventoryItem)) //must be a kind of inventory
|
||||
{
|
||||
sc.ScriptMessage("'%s' is not a type of inventory item.", sc.String);
|
||||
inventoryItem = RUNTIME_CLASS(AInventory);
|
||||
}
|
||||
}
|
||||
ParseComparator(sc);
|
||||
}
|
||||
else if(sc.Compare("ammo1"))
|
||||
type = AMMO1;
|
||||
|
@ -2328,11 +2312,11 @@ class CommandDrawBar : public SBarInfoCommand
|
|||
{
|
||||
sc.MustGetToken(TK_Identifier);
|
||||
type = AMMO;
|
||||
inventoryItem = PClass::FindClass(sc.String);
|
||||
if(inventoryItem == NULL || !RUNTIME_CLASS(AAmmo)->IsAncestorOf(inventoryItem)) //must be a kind of ammo
|
||||
data.inventoryItem = PClass::FindClass(sc.String);
|
||||
if(data.inventoryItem == NULL || !RUNTIME_CLASS(AAmmo)->IsAncestorOf(data.inventoryItem)) //must be a kind of ammo
|
||||
{
|
||||
sc.ScriptMessage("'%s' is not a type of ammo.", sc.String);
|
||||
inventoryItem = RUNTIME_CLASS(AAmmo);
|
||||
data.inventoryItem = RUNTIME_CLASS(AAmmo);
|
||||
}
|
||||
}
|
||||
else if(sc.Compare("frags"))
|
||||
|
@ -2351,21 +2335,21 @@ class CommandDrawBar : public SBarInfoCommand
|
|||
{
|
||||
type = POWERUPTIME;
|
||||
sc.MustGetToken(TK_Identifier);
|
||||
inventoryItem = PClass::FindClass(sc.String);
|
||||
if(inventoryItem == NULL || !RUNTIME_CLASS(APowerupGiver)->IsAncestorOf(inventoryItem))
|
||||
data.inventoryItem = PClass::FindClass(sc.String);
|
||||
if(data.inventoryItem == NULL || !RUNTIME_CLASS(APowerupGiver)->IsAncestorOf(data.inventoryItem))
|
||||
{
|
||||
sc.ScriptMessage("'%s' is not a type of PowerupGiver.", sc.String);
|
||||
inventoryItem = RUNTIME_CLASS(APowerupGiver);
|
||||
data.inventoryItem = RUNTIME_CLASS(APowerupGiver);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
type = INVENTORY;
|
||||
inventoryItem = PClass::FindClass(sc.String);
|
||||
if(inventoryItem == NULL || !RUNTIME_CLASS(AInventory)->IsAncestorOf(inventoryItem))
|
||||
data.inventoryItem = PClass::FindClass(sc.String);
|
||||
if(data.inventoryItem == NULL || !RUNTIME_CLASS(AInventory)->IsAncestorOf(data.inventoryItem))
|
||||
{
|
||||
sc.ScriptMessage("'%s' is not a type of inventory item.", sc.String);
|
||||
inventoryItem = RUNTIME_CLASS(AInventory);
|
||||
data.inventoryItem = RUNTIME_CLASS(AInventory);
|
||||
}
|
||||
}
|
||||
sc.MustGetToken(',');
|
||||
|
@ -2423,9 +2407,11 @@ class CommandDrawBar : public SBarInfoCommand
|
|||
if(value < 0) //health shouldn't display negatives
|
||||
value = 0;
|
||||
|
||||
if(inventoryItem != NULL)
|
||||
if(data.useMaximumConstant)
|
||||
max = data.value;
|
||||
else if(data.inventoryItem != NULL)
|
||||
{
|
||||
AInventory *item = statusBar->CPlayer->mo->FindInventory(inventoryItem); //max comparer
|
||||
AInventory *item = statusBar->CPlayer->mo->FindInventory(data.inventoryItem); //max comparer
|
||||
if(item != NULL)
|
||||
max = item->Amount;
|
||||
else
|
||||
|
@ -2436,9 +2422,11 @@ class CommandDrawBar : public SBarInfoCommand
|
|||
break;
|
||||
case ARMOR:
|
||||
value = statusBar->armor != NULL ? statusBar->armor->Amount : 0;
|
||||
if(inventoryItem != NULL)
|
||||
if(data.useMaximumConstant)
|
||||
max = data.value;
|
||||
else if(data.inventoryItem != NULL)
|
||||
{
|
||||
AInventory *item = statusBar->CPlayer->mo->FindInventory(inventoryItem);
|
||||
AInventory *item = statusBar->CPlayer->mo->FindInventory(data.inventoryItem);
|
||||
if(item != NULL)
|
||||
max = item->Amount;
|
||||
else
|
||||
|
@ -2469,7 +2457,7 @@ class CommandDrawBar : public SBarInfoCommand
|
|||
break;
|
||||
case AMMO:
|
||||
{
|
||||
AInventory *item = statusBar->CPlayer->mo->FindInventory(inventoryItem);
|
||||
AInventory *item = statusBar->CPlayer->mo->FindInventory(data.inventoryItem);
|
||||
if(item != NULL)
|
||||
{
|
||||
value = item->Amount;
|
||||
|
@ -2497,7 +2485,7 @@ class CommandDrawBar : public SBarInfoCommand
|
|||
break;
|
||||
case INVENTORY:
|
||||
{
|
||||
AInventory *item = statusBar->CPlayer->mo->FindInventory(inventoryItem);
|
||||
AInventory *item = statusBar->CPlayer->mo->FindInventory(data.inventoryItem);
|
||||
if(item != NULL)
|
||||
{
|
||||
value = item->Amount;
|
||||
|
@ -2514,7 +2502,7 @@ class CommandDrawBar : public SBarInfoCommand
|
|||
case POWERUPTIME:
|
||||
{
|
||||
//Get the PowerupType and check to see if the player has any in inventory.
|
||||
APowerupGiver *powerupGiver = (APowerupGiver*) GetDefaultByType(inventoryItem);
|
||||
APowerupGiver *powerupGiver = (APowerupGiver*) GetDefaultByType(data.inventoryItem);
|
||||
const PClass *powerupType = powerupGiver->PowerupType;
|
||||
APowerup *powerup = (APowerup*) statusBar->CPlayer->mo->FindInventory(powerupType);
|
||||
if(powerup != NULL && powerupType != NULL && powerupGiver != NULL)
|
||||
|
@ -2566,6 +2554,29 @@ class CommandDrawBar : public SBarInfoCommand
|
|||
drawValue = value;
|
||||
}
|
||||
protected:
|
||||
void ParseComparator(FScanner &sc)
|
||||
{
|
||||
bool extendedSyntax = sc.CheckToken('(');
|
||||
|
||||
if(sc.CheckToken(TK_Identifier)) //comparing reference
|
||||
{
|
||||
data.inventoryItem = PClass::FindClass(sc.String);
|
||||
if(data.inventoryItem == NULL || !RUNTIME_CLASS(AInventory)->IsAncestorOf(data.inventoryItem)) //must be a kind of inventory
|
||||
{
|
||||
sc.ScriptMessage("'%s' is not a type of inventory item.", sc.String);
|
||||
data.inventoryItem = RUNTIME_CLASS(AInventory);
|
||||
}
|
||||
}
|
||||
else if(extendedSyntax && sc.CheckToken(TK_IntConst))
|
||||
{
|
||||
data.useMaximumConstant = true;
|
||||
data.value = sc.Number;
|
||||
}
|
||||
|
||||
if(extendedSyntax)
|
||||
sc.MustGetToken(')');
|
||||
}
|
||||
|
||||
enum ValueType
|
||||
{
|
||||
HEALTH,
|
||||
|
@ -2584,13 +2595,28 @@ class CommandDrawBar : public SBarInfoCommand
|
|||
SAVEPERCENT
|
||||
};
|
||||
|
||||
struct AdditionalData
|
||||
{
|
||||
public:
|
||||
AdditionalData() : useMaximumConstant(false)
|
||||
{
|
||||
}
|
||||
|
||||
bool useMaximumConstant;
|
||||
union
|
||||
{
|
||||
const PClass *inventoryItem;
|
||||
int value;
|
||||
};
|
||||
};
|
||||
|
||||
unsigned int border;
|
||||
bool horizontal;
|
||||
bool reverse;
|
||||
int foreground;
|
||||
int background;
|
||||
ValueType type;
|
||||
const PClass *inventoryItem;
|
||||
AdditionalData data;
|
||||
SBarInfoCoordinate x;
|
||||
SBarInfoCoordinate y;
|
||||
|
||||
|
|
|
@ -66,6 +66,7 @@ void FMapInfoParser::ParseSkill ()
|
|||
skill.FastMonsters = false;
|
||||
skill.DisableCheats = false;
|
||||
skill.EasyBossBrain = false;
|
||||
skill.EasyKey = false;
|
||||
skill.AutoUseHealth = false;
|
||||
skill.RespawnCounter = 0;
|
||||
skill.RespawnLimit = 0;
|
||||
|
@ -125,6 +126,10 @@ void FMapInfoParser::ParseSkill ()
|
|||
{
|
||||
skill.EasyBossBrain = true;
|
||||
}
|
||||
else if (sc.Compare ("easykey"))
|
||||
{
|
||||
skill.EasyKey = true;
|
||||
}
|
||||
else if (sc.Compare("autousehealth"))
|
||||
{
|
||||
skill.AutoUseHealth = true;
|
||||
|
@ -351,6 +356,9 @@ int G_SkillProperty(ESkillProperty prop)
|
|||
case SKILLP_EasyBossBrain:
|
||||
return AllSkills[gameskill].EasyBossBrain;
|
||||
|
||||
case SKILLP_EasyKey:
|
||||
return AllSkills[gameskill].EasyKey;
|
||||
|
||||
case SKILLP_SpawnFilter:
|
||||
return AllSkills[gameskill].SpawnFilter;
|
||||
|
||||
|
@ -428,6 +436,7 @@ FSkillInfo &FSkillInfo::operator=(const FSkillInfo &other)
|
|||
DisableCheats = other.DisableCheats;
|
||||
AutoUseHealth = other.AutoUseHealth;
|
||||
EasyBossBrain = other.EasyBossBrain;
|
||||
EasyKey = other.EasyKey;
|
||||
RespawnCounter= other.RespawnCounter;
|
||||
RespawnLimit= other.RespawnLimit;
|
||||
Aggressiveness= other.Aggressiveness;
|
||||
|
|
133
src/p_acs.cpp
133
src/p_acs.cpp
|
@ -1529,11 +1529,11 @@ void FBehavior::LoadScriptsDirectory ()
|
|||
}
|
||||
}
|
||||
|
||||
// Load script var counts
|
||||
// Load script var counts. (Only recorded for scripts that use more than LOCAL_SIZE variables.)
|
||||
scripts.b = FindChunk (MAKE_ID('S','V','C','T'));
|
||||
if (scripts.dw != NULL)
|
||||
{
|
||||
max = scripts.dw[1];
|
||||
max = scripts.dw[1] / 4;
|
||||
scripts.dw += 2;
|
||||
for (i = max; i > 0; --i, scripts.w += 2)
|
||||
{
|
||||
|
@ -1544,6 +1544,27 @@ void FBehavior::LoadScriptsDirectory ()
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load script names (if any)
|
||||
scripts.b = FindChunk(MAKE_ID('S','N','A','M'));
|
||||
if (scripts.dw != NULL)
|
||||
{
|
||||
for (i = 0; i < NumScripts; ++i)
|
||||
{
|
||||
// ACC stores script names as an index into the SNAM chunk, with the first index as
|
||||
// -1 and counting down from there. We convert this from an index into SNAM into
|
||||
// a negative index into the global name table.
|
||||
if (Scripts[i].Number < 0)
|
||||
{
|
||||
const char *str = (const char *)(scripts.b + 8 + scripts.dw[3 + (-Scripts[i].Number - 1)]);
|
||||
FName name(str);
|
||||
Scripts[i].Number = -name;
|
||||
}
|
||||
}
|
||||
// We need to resort scripts, because the new numbers for named scripts likely
|
||||
// do not match the order they were originally in.
|
||||
qsort (Scripts, NumScripts, sizeof(ScriptPtr), SortScripts);
|
||||
}
|
||||
}
|
||||
|
||||
int STACK_ARGS FBehavior::SortScripts (const void *a, const void *b)
|
||||
|
@ -1621,8 +1642,8 @@ bool FBehavior::IsGood ()
|
|||
|
||||
const ScriptPtr *FBehavior::FindScript (int script) const
|
||||
{
|
||||
const ScriptPtr *ptr = BinarySearch<ScriptPtr, WORD>
|
||||
((ScriptPtr *)Scripts, NumScripts, &ScriptPtr::Number, (WORD)script);
|
||||
const ScriptPtr *ptr = BinarySearch<ScriptPtr, int>
|
||||
((ScriptPtr *)Scripts, NumScripts, &ScriptPtr::Number, script);
|
||||
|
||||
// If the preceding script has the same number, return it instead.
|
||||
// See the note by the script sorting above for why.
|
||||
|
@ -1859,6 +1880,50 @@ void FBehavior::StaticStopMyScripts (AActor *actor)
|
|||
}
|
||||
}
|
||||
|
||||
//==========================================================================
|
||||
//
|
||||
// SerializeScriptNumber
|
||||
//
|
||||
// Serializes a script number. If it's negative, it's really a name, so
|
||||
// that will get serialized after it.
|
||||
//
|
||||
//==========================================================================
|
||||
|
||||
static void SerializeScriptNumber(FArchive &arc, int &scriptnum, bool was2byte)
|
||||
{
|
||||
if (SaveVersion < 3359)
|
||||
{
|
||||
if (was2byte)
|
||||
{
|
||||
WORD oldver;
|
||||
arc << oldver;
|
||||
scriptnum = oldver;
|
||||
}
|
||||
else
|
||||
{
|
||||
arc << scriptnum;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
arc << scriptnum;
|
||||
// If the script number is negative, then it's really a name.
|
||||
// So read/store the name after it.
|
||||
if (scriptnum < 0)
|
||||
{
|
||||
if (arc.IsStoring())
|
||||
{
|
||||
arc.WriteName(FName(ENamedName(-scriptnum)).GetChars());
|
||||
}
|
||||
else
|
||||
{
|
||||
const char *nam = arc.ReadName();
|
||||
scriptnum = -FName(nam);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//---- The ACS Interpreter ----//
|
||||
|
||||
IMPLEMENT_POINTY_CLASS (DACSThinker)
|
||||
|
@ -1880,8 +1945,7 @@ DACSThinker::DACSThinker ()
|
|||
ActiveThinker = this;
|
||||
Scripts = NULL;
|
||||
LastScript = NULL;
|
||||
for (int i = 0; i < 1000; i++)
|
||||
RunningScripts[i] = NULL;
|
||||
RunningScripts.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1893,27 +1957,34 @@ DACSThinker::~DACSThinker ()
|
|||
|
||||
void DACSThinker::Serialize (FArchive &arc)
|
||||
{
|
||||
int scriptnum;
|
||||
|
||||
Super::Serialize (arc);
|
||||
arc << Scripts << LastScript;
|
||||
if (arc.IsStoring ())
|
||||
{
|
||||
WORD i;
|
||||
for (i = 0; i < 1000; i++)
|
||||
ScriptMap::Iterator it(RunningScripts);
|
||||
ScriptMap::Pair *pair;
|
||||
|
||||
while (it.NextPair(pair))
|
||||
{
|
||||
if (RunningScripts[i])
|
||||
arc << RunningScripts[i] << i;
|
||||
assert(pair->Value != NULL);
|
||||
arc << pair->Value;
|
||||
scriptnum = pair->Key;
|
||||
SerializeScriptNumber(arc, scriptnum, true);
|
||||
}
|
||||
DLevelScript *nilptr = NULL;
|
||||
arc << nilptr;
|
||||
}
|
||||
else
|
||||
else // Loading
|
||||
{
|
||||
WORD scriptnum;
|
||||
DLevelScript *script = NULL;
|
||||
RunningScripts.Clear();
|
||||
|
||||
arc << script;
|
||||
while (script)
|
||||
{
|
||||
arc << scriptnum;
|
||||
SerializeScriptNumber(arc, scriptnum, true);
|
||||
RunningScripts[scriptnum] = script;
|
||||
arc << script;
|
||||
}
|
||||
|
@ -1975,8 +2046,9 @@ void DLevelScript::Serialize (FArchive &arc)
|
|||
DWORD i;
|
||||
|
||||
Super::Serialize (arc);
|
||||
arc << next << prev
|
||||
<< script;
|
||||
arc << next << prev;
|
||||
|
||||
SerializeScriptNumber(arc, script, false);
|
||||
|
||||
arc << state
|
||||
<< statedata
|
||||
|
@ -3713,13 +3785,13 @@ int DLevelScript::RunScript ()
|
|||
|
||||
case SCRIPT_ScriptWaitPre:
|
||||
// Wait for a script to start running, then enter state scriptwait
|
||||
if (controller->RunningScripts[statedata])
|
||||
if (controller->RunningScripts.CheckKey(statedata) != NULL)
|
||||
state = SCRIPT_ScriptWait;
|
||||
break;
|
||||
|
||||
case SCRIPT_ScriptWait:
|
||||
// Wait for a script to stop running, then enter state running
|
||||
if (controller->RunningScripts[statedata])
|
||||
if (controller->RunningScripts.CheckKey(statedata) != NULL)
|
||||
return resultValue;
|
||||
|
||||
state = SCRIPT_Running;
|
||||
|
@ -5016,7 +5088,7 @@ int DLevelScript::RunScript ()
|
|||
|
||||
case PCD_SCRIPTWAIT:
|
||||
statedata = STACK(1);
|
||||
if (controller->RunningScripts[statedata])
|
||||
if (controller->RunningScripts.CheckKey(statedata) != NULL)
|
||||
state = SCRIPT_ScriptWait;
|
||||
else
|
||||
state = SCRIPT_ScriptWaitPre;
|
||||
|
@ -6960,8 +7032,12 @@ int DLevelScript::RunScript ()
|
|||
if (state == SCRIPT_PleaseRemove)
|
||||
{
|
||||
Unlink ();
|
||||
if (controller->RunningScripts[script] == this)
|
||||
controller->RunningScripts[script] = NULL;
|
||||
DLevelScript **running;
|
||||
if ((running = controller->RunningScripts.CheckKey(script)) != NULL &&
|
||||
*running == this)
|
||||
{
|
||||
controller->RunningScripts.Remove(script);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
|
@ -6977,13 +7053,14 @@ static DLevelScript *P_GetScriptGoing (AActor *who, line_t *where, int num, cons
|
|||
bool backSide, int arg0, int arg1, int arg2, int always)
|
||||
{
|
||||
DACSThinker *controller = DACSThinker::ActiveThinker;
|
||||
DLevelScript **running;
|
||||
|
||||
if (controller && !always && controller->RunningScripts[num])
|
||||
if (controller && !always && (running = controller->RunningScripts.CheckKey(num)) != NULL)
|
||||
{
|
||||
if (controller->RunningScripts[num]->GetState () == DLevelScript::SCRIPT_Suspended)
|
||||
if ((*running)->GetState() == DLevelScript::SCRIPT_Suspended)
|
||||
{
|
||||
controller->RunningScripts[num]->SetState (DLevelScript::SCRIPT_Running);
|
||||
return controller->RunningScripts[num];
|
||||
(*running)->SetState(DLevelScript::SCRIPT_Running);
|
||||
return *running;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
@ -6999,6 +7076,7 @@ DLevelScript::DLevelScript (AActor *who, line_t *where, int num, const ScriptPtr
|
|||
new DACSThinker;
|
||||
|
||||
script = num;
|
||||
assert(code->VarCount >= code->ArgCount);
|
||||
numlocalvars = code->VarCount;
|
||||
localvars = new SDWORD[code->VarCount];
|
||||
if (code->VarCount > 0)
|
||||
|
@ -7043,9 +7121,12 @@ DLevelScript::DLevelScript (AActor *who, line_t *where, int num, const ScriptPtr
|
|||
static void SetScriptState (int script, DLevelScript::EScriptState state)
|
||||
{
|
||||
DACSThinker *controller = DACSThinker::ActiveThinker;
|
||||
DLevelScript **running;
|
||||
|
||||
if (controller != NULL && controller->RunningScripts[script])
|
||||
controller->RunningScripts[script]->SetState (state);
|
||||
if (controller != NULL && (running = controller->RunningScripts.CheckKey(script)) != NULL)
|
||||
{
|
||||
(*running)->SetState (state);
|
||||
}
|
||||
}
|
||||
|
||||
void P_DoDeferedScripts ()
|
||||
|
|
|
@ -76,18 +76,18 @@ void P_ClearACSVars(bool);
|
|||
// The in-memory version
|
||||
struct ScriptPtr
|
||||
{
|
||||
WORD Number;
|
||||
int Number;
|
||||
DWORD Address;
|
||||
BYTE Type;
|
||||
BYTE ArgCount;
|
||||
WORD VarCount;
|
||||
WORD Flags;
|
||||
DWORD Address;
|
||||
};
|
||||
|
||||
// The present ZDoom version
|
||||
struct ScriptPtr3
|
||||
{
|
||||
WORD Number;
|
||||
SWORD Number;
|
||||
BYTE Type;
|
||||
BYTE ArgCount;
|
||||
DWORD Address;
|
||||
|
@ -747,7 +747,8 @@ public:
|
|||
void Serialize (FArchive &arc);
|
||||
void Tick ();
|
||||
|
||||
DLevelScript *RunningScripts[1000]; // Array of all synchronous scripts
|
||||
typedef TMap<int, DLevelScript *> ScriptMap;
|
||||
ScriptMap RunningScripts; // Array of all synchronous scripts
|
||||
static TObjPtr<DACSThinker> ActiveThinker;
|
||||
|
||||
void DumpScriptStatus();
|
||||
|
|
|
@ -1295,7 +1295,8 @@ bool FPolyObj::CheckMobjBlocking (side_t *sd)
|
|||
fixed_t top = -INT_MAX, bottom = INT_MAX;
|
||||
bool above;
|
||||
// [TN] Check wether this actor gets blocked by the line.
|
||||
if(!(ld->flags & (ML_BLOCKING|ML_BLOCKEVERYTHING))
|
||||
if (ld->backsector != NULL &&
|
||||
!(ld->flags & (ML_BLOCKING|ML_BLOCKEVERYTHING))
|
||||
&& !(ld->flags & ML_BLOCK_PLAYERS && mobj->player)
|
||||
&& !(ld->flags & ML_BLOCKMONSTERS && mobj->flags3 & MF3_ISMONSTER)
|
||||
&& !((mobj->flags & MF_FLOAT) && (ld->flags & ML_BLOCK_FLOATERS))
|
||||
|
|
|
@ -101,7 +101,7 @@ extern float S_GetMusicVolume (const char *music);
|
|||
|
||||
// PRIVATE FUNCTION PROTOTYPES ---------------------------------------------
|
||||
|
||||
static bool S_CheckSoundLimit(sfxinfo_t *sfx, const FVector3 &pos, int near_limit, float limit_range);
|
||||
static bool S_CheckSoundLimit(sfxinfo_t *sfx, const FVector3 &pos, int near_limit, float limit_range, AActor *actor, int channel);
|
||||
static bool S_IsChannelUsed(AActor *actor, int channel, int *seen);
|
||||
static void S_ActivatePlayList(bool goBack);
|
||||
static void CalcPosVel(FSoundChan *chan, FVector3 *pos, FVector3 *vel);
|
||||
|
@ -952,7 +952,7 @@ static FSoundChan *S_StartSound(AActor *actor, const sector_t *sec, const FPolyO
|
|||
|
||||
// If this sound doesn't like playing near itself, don't play it if
|
||||
// that's what would happen.
|
||||
if (near_limit > 0 && S_CheckSoundLimit(sfx, pos, near_limit, limit_range))
|
||||
if (near_limit > 0 && S_CheckSoundLimit(sfx, pos, near_limit, limit_range, actor, channel))
|
||||
{
|
||||
chanflags |= CHAN_EVICTED;
|
||||
}
|
||||
|
@ -1157,7 +1157,7 @@ void S_RestartSound(FSoundChan *chan)
|
|||
|
||||
// If this sound doesn't like playing near itself, don't play it if
|
||||
// that's what would happen.
|
||||
if (chan->NearLimit > 0 && S_CheckSoundLimit(&S_sfx[chan->SoundID], pos, chan->NearLimit, chan->LimitRange))
|
||||
if (chan->NearLimit > 0 && S_CheckSoundLimit(&S_sfx[chan->SoundID], pos, chan->NearLimit, chan->LimitRange, NULL, 0))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
@ -1389,12 +1389,19 @@ bool S_CheckSingular(int sound_id)
|
|||
//
|
||||
// Limits the number of nearby copies of a sound that can play near
|
||||
// each other. If there are NearLimit instances of this sound already
|
||||
// playing within 256 units of the new sound, the new sound will not
|
||||
// start.
|
||||
// playing within sqrt(limit_range) (typically 256 units) of the new sound, the
|
||||
// new sound will not start.
|
||||
//
|
||||
// If an actor is specified, and it is already playing the same sound on
|
||||
// the same channel, this sound will not be limited. In this case, we're
|
||||
// restarting an already playing sound, so there's no need to limit it.
|
||||
//
|
||||
// Returns true if the sound should not play.
|
||||
//
|
||||
//==========================================================================
|
||||
|
||||
bool S_CheckSoundLimit(sfxinfo_t *sfx, const FVector3 &pos, int near_limit, float limit_range)
|
||||
bool S_CheckSoundLimit(sfxinfo_t *sfx, const FVector3 &pos, int near_limit, float limit_range,
|
||||
AActor *actor, int channel)
|
||||
{
|
||||
FSoundChan *chan;
|
||||
int count;
|
||||
|
@ -1405,6 +1412,12 @@ bool S_CheckSoundLimit(sfxinfo_t *sfx, const FVector3 &pos, int near_limit, floa
|
|||
{
|
||||
FVector3 chanorigin;
|
||||
|
||||
if (actor != NULL && chan->EntChannel == channel &&
|
||||
chan->SourceType == SOURCE_Actor && chan->Actor == actor)
|
||||
{ // We are restarting a playing sound. Always let it play.
|
||||
return false;
|
||||
}
|
||||
|
||||
CalcPosVel(chan, &chanorigin, NULL);
|
||||
if ((chanorigin - pos).LengthSquared() <= limit_range)
|
||||
{
|
||||
|
|
|
@ -1725,8 +1725,6 @@ FISoundChannel *FMODSoundRenderer::StartSound(SoundHandle sfx, float vol, int pi
|
|||
//
|
||||
//==========================================================================
|
||||
|
||||
CVAR(Float, snd_3dspread, 180, 0)
|
||||
|
||||
FISoundChannel *FMODSoundRenderer::StartSound3D(SoundHandle sfx, SoundListener *listener, float vol,
|
||||
FRolloffInfo *rolloff, float distscale,
|
||||
int pitch, int priority, const FVector3 &pos, const FVector3 &vel,
|
||||
|
@ -1811,7 +1809,6 @@ FISoundChannel *FMODSoundRenderer::StartSound3D(SoundHandle sfx, SoundListener *
|
|||
if (mode & FMOD_3D)
|
||||
{
|
||||
chan->set3DAttributes((FMOD_VECTOR *)&pos[0], (FMOD_VECTOR *)&vel[0]);
|
||||
chan->set3DSpread(snd_3dspread);
|
||||
}
|
||||
if (!HandleChannelDelay(chan, reuse_chan, flags & (SNDF_ABSTIME | SNDF_LOOP), freq))
|
||||
{
|
||||
|
|
|
@ -3,5 +3,5 @@
|
|||
// This file was automatically generated by the
|
||||
// updaterevision tool. Do not edit by hand.
|
||||
|
||||
#define ZD_SVN_REVISION_STRING "3350"
|
||||
#define ZD_SVN_REVISION_NUMBER 3350
|
||||
#define ZD_SVN_REVISION_STRING "3360"
|
||||
#define ZD_SVN_REVISION_NUMBER 3360
|
||||
|
|
|
@ -78,7 +78,7 @@ void STACK_ARGS DCanvas::DrawChar (FFont *font, int normalcolor, int x, int y, B
|
|||
//
|
||||
// Write a string using the given font
|
||||
//
|
||||
void DCanvas::DrawTextV(FFont *font, int normalcolor, int x, int y, const char *string, uint32 tag1, va_list taglist)
|
||||
void DCanvas::DrawTextV(FFont *font, int normalcolor, int x, int y, const char *string, va_list taglist)
|
||||
{
|
||||
INTBOOL boolval;
|
||||
va_list tags;
|
||||
|
@ -122,7 +122,7 @@ void DCanvas::DrawTextV(FFont *font, int normalcolor, int x, int y, const char *
|
|||
#else
|
||||
tags = taglist;
|
||||
#endif
|
||||
tag = tag1;
|
||||
tag = va_arg(tags, uint32);
|
||||
|
||||
while (tag != TAG_DONE)
|
||||
{
|
||||
|
@ -209,6 +209,7 @@ void DCanvas::DrawTextV(FFont *font, int normalcolor, int x, int y, const char *
|
|||
}
|
||||
tag = va_arg (tags, uint32);
|
||||
}
|
||||
va_end(tags);
|
||||
|
||||
height *= scaley;
|
||||
|
||||
|
@ -242,7 +243,6 @@ void DCanvas::DrawTextV(FFont *font, int normalcolor, int x, int y, const char *
|
|||
#else
|
||||
tags = taglist;
|
||||
#endif
|
||||
tag = tag1;
|
||||
if (forcedwidth)
|
||||
{
|
||||
w = forcedwidth;
|
||||
|
@ -262,21 +262,22 @@ void DCanvas::DrawTextV(FFont *font, int normalcolor, int x, int y, const char *
|
|||
}
|
||||
cx += (w + kerning) * scalex;
|
||||
}
|
||||
va_end(taglist);
|
||||
}
|
||||
|
||||
void STACK_ARGS DCanvas::DrawText (FFont *font, int normalcolor, int x, int y, const char *string, uint32 tag, ...)
|
||||
void STACK_ARGS DCanvas::DrawText (FFont *font, int normalcolor, int x, int y, const char *string, ...)
|
||||
{
|
||||
va_list tags;
|
||||
va_start(tags, tag);
|
||||
DrawTextV(font, normalcolor, x, y, string, tag, tags);
|
||||
va_start(tags, string);
|
||||
DrawTextV(font, normalcolor, x, y, string, tags);
|
||||
}
|
||||
|
||||
// A synonym so that this can still be used in files that #include Windows headers
|
||||
void STACK_ARGS DCanvas::DrawTextA (FFont *font, int normalcolor, int x, int y, const char *string, uint32 tag, ...)
|
||||
void STACK_ARGS DCanvas::DrawTextA (FFont *font, int normalcolor, int x, int y, const char *string, ...)
|
||||
{
|
||||
va_list tags;
|
||||
va_start(tags, tag);
|
||||
DrawTextV(font, normalcolor, x, y, string, tag, tags);
|
||||
va_start(tags, string);
|
||||
DrawTextV(font, normalcolor, x, y, string, tags);
|
||||
}
|
||||
|
||||
//
|
||||
|
|
|
@ -214,11 +214,11 @@ public:
|
|||
void VirtualToRealCoordsInt(int &x, int &y, int &w, int &h, int vwidth, int vheight, bool vbottom=false, bool handleaspect=true) const;
|
||||
|
||||
// 2D Text drawing
|
||||
void STACK_ARGS DrawText (FFont *font, int normalcolor, int x, int y, const char *string, uint32 tag, ...);
|
||||
void STACK_ARGS DrawText (FFont *font, int normalcolor, int x, int y, const char *string, ...);
|
||||
#ifndef DrawText // See WinUser.h for the definition of DrawText as a macro
|
||||
void STACK_ARGS DrawTextA (FFont *font, int normalcolor, int x, int y, const char *string, uint32 tag, ...);
|
||||
void STACK_ARGS DrawTextA (FFont *font, int normalcolor, int x, int y, const char *string, ...);
|
||||
#endif
|
||||
void DrawTextV (FFont *font, int normalcolor, int x, int y, const char *string, uint32 tag, va_list tags);
|
||||
void DrawTextV (FFont *font, int normalcolor, int x, int y, const char *string, va_list tags);
|
||||
void STACK_ARGS DrawChar (FFont *font, int normalcolor, int x, int y, BYTE character, ...);
|
||||
|
||||
struct DrawParms
|
||||
|
|
9
wadsrc/static/maparrows/ravenkey.txt
Normal file
9
wadsrc/static/maparrows/ravenkey.txt
Normal file
|
@ -0,0 +1,9 @@
|
|||
( 0, 0 ), ( 0.25, -0.5 )
|
||||
( 0.25, -0.5 ), ( 0.5, -0.5 )
|
||||
( 0.5, -0.5 ), ( 0.5, 0.5 )
|
||||
( 0.5, 0.5 ), ( 0.25, 0.5 )
|
||||
( 0.25, 0.5 ), ( 0, 0 ) // handle part type thing
|
||||
( 0, 0 ), ( -1, 0 ) // stem
|
||||
( -1, 0 ), ( -1, -0.5 ) // end lockpick part
|
||||
( -0.75, 0 ), ( -0.75, -0.25 )
|
||||
|
|
@ -75,6 +75,7 @@ skill baby
|
|||
EasyBossBrain
|
||||
SpawnFilter = Baby
|
||||
Name = "$MNU_WETNURSE"
|
||||
EasyKey
|
||||
}
|
||||
|
||||
skill easy
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue