mirror of
https://github.com/yquake2/yquake2remaster.git
synced 2024-11-10 07:12:07 +00:00
game: sync ctf player
This commit is contained in:
parent
802a874f64
commit
d27fc1f333
7 changed files with 179 additions and 4630 deletions
8
Makefile
8
Makefile
|
@ -1497,7 +1497,6 @@ CTF_OBJS_ = \
|
|||
src/game/g_misc.o \
|
||||
src/game/g_monster.o \
|
||||
src/game/g_phys.o \
|
||||
src/ctf/g_save.o \
|
||||
src/game/g_sphere.o \
|
||||
src/game/g_spawn.o \
|
||||
src/game/g_svcmds.o \
|
||||
|
@ -1540,11 +1539,12 @@ CTF_OBJS_ = \
|
|||
src/game/monster/turret/turret.o \
|
||||
src/game/monster/widow/widow2.o \
|
||||
src/game/monster/widow/widow.o \
|
||||
src/ctf/player/client.o \
|
||||
src/game/player/client.o \
|
||||
src/game/player/hud.o \
|
||||
src/game/player/trail.o \
|
||||
src/ctf/player/view.o \
|
||||
src/game/player/weapon.o
|
||||
src/game/player/view.o \
|
||||
src/game/player/weapon.o \
|
||||
src/game/savegame/savegame.o
|
||||
|
||||
# ----------
|
||||
|
||||
|
|
819
src/ctf/g_save.c
819
src/ctf/g_save.c
|
@ -1,819 +0,0 @@
|
|||
/*
|
||||
* Copyright (C) 1997-2001 Id Software, Inc.
|
||||
* Copyright (C) 2011 Knightmare
|
||||
* Copyright (C) 2011 Yamagi Burmeister
|
||||
* Copyright (c) ZeniMax Media Inc.
|
||||
*
|
||||
* 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 the Free Software
|
||||
* Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA
|
||||
* 02111-1307, USA.
|
||||
*
|
||||
* =======================================================================
|
||||
*
|
||||
* The savegame system. Unused by the CTF game but nevertheless called
|
||||
* during game initialization. Therefor no new savegame code ist
|
||||
* imported.
|
||||
*
|
||||
* =======================================================================
|
||||
*/
|
||||
|
||||
#include "header/local.h"
|
||||
|
||||
#ifndef BUILD_DATE
|
||||
#define BUILD_DATE __DATE__
|
||||
#endif
|
||||
|
||||
field_t fields[] = {
|
||||
{"classname", FOFS(classname), F_LSTRING},
|
||||
{"origin", FOFS(s.origin), F_VECTOR},
|
||||
{"model", FOFS(model), F_LSTRING},
|
||||
{"spawnflags", FOFS(spawnflags), F_INT},
|
||||
{"speed", FOFS(speed), F_FLOAT},
|
||||
{"accel", FOFS(accel), F_FLOAT},
|
||||
{"decel", FOFS(decel), F_FLOAT},
|
||||
{"target", FOFS(target), F_LSTRING},
|
||||
{"targetname", FOFS(targetname), F_LSTRING},
|
||||
{"pathtarget", FOFS(pathtarget), F_LSTRING},
|
||||
{"deathtarget", FOFS(deathtarget), F_LSTRING},
|
||||
{"killtarget", FOFS(killtarget), F_LSTRING},
|
||||
{"combattarget", FOFS(combattarget), F_LSTRING},
|
||||
{"message", FOFS(message), F_LSTRING},
|
||||
{"team", FOFS(team), F_LSTRING},
|
||||
{"wait", FOFS(wait), F_FLOAT},
|
||||
{"delay", FOFS(delay), F_FLOAT},
|
||||
{"random", FOFS(random), F_FLOAT},
|
||||
{"move_origin", FOFS(move_origin), F_VECTOR},
|
||||
{"move_angles", FOFS(move_angles), F_VECTOR},
|
||||
{"style", FOFS(style), F_INT},
|
||||
{"count", FOFS(count), F_INT},
|
||||
{"health", FOFS(health), F_INT},
|
||||
{"sounds", FOFS(sounds), F_INT},
|
||||
{"light", 0, F_IGNORE},
|
||||
{"dmg", FOFS(dmg), F_INT},
|
||||
{"angles", FOFS(s.angles), F_VECTOR},
|
||||
{"angle", FOFS(s.angles), F_ANGLEHACK},
|
||||
{"mass", FOFS(mass), F_INT},
|
||||
{"volume", FOFS(volume), F_FLOAT},
|
||||
{"attenuation", FOFS(attenuation), F_FLOAT},
|
||||
{"map", FOFS(map), F_LSTRING},
|
||||
|
||||
/* temp spawn vars -- only valid when the spawn function is called */
|
||||
{"lip", STOFS(lip), F_INT, FFL_SPAWNTEMP},
|
||||
{"distance", STOFS(distance), F_INT, FFL_SPAWNTEMP},
|
||||
{"height", STOFS(height), F_INT, FFL_SPAWNTEMP},
|
||||
{"noise", STOFS(noise), F_LSTRING, FFL_SPAWNTEMP},
|
||||
{"pausetime", STOFS(pausetime), F_FLOAT, FFL_SPAWNTEMP},
|
||||
{"item", STOFS(item), F_LSTRING, FFL_SPAWNTEMP},
|
||||
{"gravity", STOFS(gravity), F_LSTRING, FFL_SPAWNTEMP},
|
||||
{"sky", STOFS(sky), F_LSTRING, FFL_SPAWNTEMP},
|
||||
{"skyrotate", STOFS(skyrotate), F_FLOAT, FFL_SPAWNTEMP},
|
||||
{"skyaxis", STOFS(skyaxis), F_VECTOR, FFL_SPAWNTEMP},
|
||||
{"minyaw", STOFS(minyaw), F_FLOAT, FFL_SPAWNTEMP},
|
||||
{"maxyaw", STOFS(maxyaw), F_FLOAT, FFL_SPAWNTEMP},
|
||||
{"minpitch", STOFS(minpitch), F_FLOAT, FFL_SPAWNTEMP},
|
||||
{"maxpitch", STOFS(maxpitch), F_FLOAT, FFL_SPAWNTEMP},
|
||||
{"nextmap", STOFS(nextmap), F_LSTRING, FFL_SPAWNTEMP}
|
||||
};
|
||||
|
||||
field_t savefields[] = {
|
||||
{"", FOFS(classname), F_LSTRING},
|
||||
{"", FOFS(target), F_LSTRING},
|
||||
{"", FOFS(targetname), F_LSTRING},
|
||||
{"", FOFS(killtarget), F_LSTRING},
|
||||
{"", FOFS(team), F_LSTRING},
|
||||
{"", FOFS(pathtarget), F_LSTRING},
|
||||
{"", FOFS(deathtarget), F_LSTRING},
|
||||
{"", FOFS(combattarget), F_LSTRING},
|
||||
{"", FOFS(model), F_LSTRING},
|
||||
{"", FOFS(map), F_LSTRING},
|
||||
{"", FOFS(message), F_LSTRING},
|
||||
|
||||
{"", FOFS(client), F_CLIENT},
|
||||
{"", FOFS(item), F_ITEM},
|
||||
|
||||
{"", FOFS(goalentity), F_EDICT},
|
||||
{"", FOFS(movetarget), F_EDICT},
|
||||
{"", FOFS(enemy), F_EDICT},
|
||||
{"", FOFS(oldenemy), F_EDICT},
|
||||
{"", FOFS(activator), F_EDICT},
|
||||
{"", FOFS(groundentity), F_EDICT},
|
||||
{"", FOFS(teamchain), F_EDICT},
|
||||
{"", FOFS(teammaster), F_EDICT},
|
||||
{"", FOFS(owner), F_EDICT},
|
||||
{"", FOFS(mynoise), F_EDICT},
|
||||
{"", FOFS(mynoise2), F_EDICT},
|
||||
{"", FOFS(target_ent), F_EDICT},
|
||||
{"", FOFS(chain), F_EDICT},
|
||||
|
||||
{NULL, 0, F_INT}
|
||||
};
|
||||
|
||||
field_t levelfields[] = {
|
||||
{"", LLOFS(changemap), F_LSTRING},
|
||||
|
||||
{"", LLOFS(sight_client), F_EDICT},
|
||||
{"", LLOFS(sight_entity), F_EDICT},
|
||||
{"", LLOFS(sound_entity), F_EDICT},
|
||||
{"", LLOFS(sound2_entity), F_EDICT},
|
||||
|
||||
{NULL, 0, F_INT}
|
||||
};
|
||||
|
||||
field_t clientfields[] = {
|
||||
{"", CLOFS(pers.weapon), F_ITEM},
|
||||
{"", CLOFS(pers.lastweapon), F_ITEM},
|
||||
{"", CLOFS(newweapon), F_ITEM},
|
||||
|
||||
{NULL, 0, F_INT}
|
||||
};
|
||||
|
||||
/*
|
||||
* This will be called when the dll is first loaded, which
|
||||
* only happens when a new game is started or a save game
|
||||
* is loaded.
|
||||
*/
|
||||
void
|
||||
InitGame(void)
|
||||
{
|
||||
gi.dprintf("Game is starting up.\n");
|
||||
gi.dprintf("Game is ctf built on %s.\n", GAMEVERSION, BUILD_DATE);
|
||||
|
||||
gun_x = gi.cvar("gun_x", "0", 0);
|
||||
gun_y = gi.cvar("gun_y", "0", 0);
|
||||
gun_z = gi.cvar("gun_z", "0", 0);
|
||||
sv_rollspeed = gi.cvar("sv_rollspeed", "200", 0);
|
||||
sv_rollangle = gi.cvar("sv_rollangle", "2", 0);
|
||||
sv_maxvelocity = gi.cvar("sv_maxvelocity", "2000", 0);
|
||||
sv_gravity = gi.cvar("sv_gravity", "800", 0);
|
||||
|
||||
/* noset vars */
|
||||
dedicated = gi.cvar("dedicated", "0", CVAR_NOSET);
|
||||
|
||||
/* latched vars */
|
||||
sv_cheats = gi.cvar("cheats", "0", CVAR_SERVERINFO | CVAR_LATCH);
|
||||
gi.cvar("gamename", GAMEVERSION, CVAR_SERVERINFO | CVAR_LATCH);
|
||||
gi.cvar("gamedate", BUILD_DATE, CVAR_SERVERINFO | CVAR_LATCH);
|
||||
maxclients = gi.cvar("maxclients", "4", CVAR_SERVERINFO | CVAR_LATCH);
|
||||
deathmatch = gi.cvar("deathmatch", "0", CVAR_LATCH);
|
||||
coop = gi.cvar("coop", "0", CVAR_LATCH);
|
||||
skill = gi.cvar("skill", "1", CVAR_LATCH);
|
||||
maxentities = gi.cvar("maxentities", "1024", CVAR_LATCH);
|
||||
|
||||
/* This game.dll only supports deathmatch */
|
||||
if (!deathmatch->value)
|
||||
{
|
||||
gi.dprintf("Forcing deathmatch.\n");
|
||||
gi.cvar_set("deathmatch", "1");
|
||||
}
|
||||
|
||||
/* force coop off */
|
||||
if (coop->value)
|
||||
{
|
||||
gi.cvar_set("coop", "0");
|
||||
}
|
||||
|
||||
/* change anytime vars */
|
||||
dmflags = gi.cvar("dmflags", "0", CVAR_SERVERINFO);
|
||||
fraglimit = gi.cvar("fraglimit", "0", CVAR_SERVERINFO);
|
||||
timelimit = gi.cvar("timelimit", "0", CVAR_SERVERINFO);
|
||||
capturelimit = gi.cvar("capturelimit", "0", CVAR_SERVERINFO);
|
||||
instantweap = gi.cvar("instantweap", "0", CVAR_SERVERINFO);
|
||||
password = gi.cvar("password", "", CVAR_USERINFO);
|
||||
spectator_password = gi.cvar("spectator_password", "", CVAR_USERINFO);
|
||||
needpass = gi.cvar("needpass", "0", CVAR_SERVERINFO);
|
||||
filterban = gi.cvar("filterban", "1", 0);
|
||||
g_select_empty = gi.cvar("g_select_empty", "0", CVAR_ARCHIVE);
|
||||
run_pitch = gi.cvar("run_pitch", "0.002", 0);
|
||||
run_roll = gi.cvar("run_roll", "0.005", 0);
|
||||
bob_up = gi.cvar("bob_up", "0.005", 0);
|
||||
bob_pitch = gi.cvar("bob_pitch", "0.002", 0);
|
||||
bob_roll = gi.cvar("bob_roll", "0.002", 0);
|
||||
|
||||
/* flood control */
|
||||
flood_msgs = gi.cvar("flood_msgs", "4", 0);
|
||||
flood_persecond = gi.cvar("flood_persecond", "4", 0);
|
||||
flood_waitdelay = gi.cvar("flood_waitdelay", "10", 0);
|
||||
|
||||
/* dm map list */
|
||||
sv_maplist = gi.cvar("sv_maplist", "", 0);
|
||||
|
||||
/* disruptor availability */
|
||||
g_disruptor = gi.cvar("g_disruptor", "0", 0);
|
||||
|
||||
/* others */
|
||||
aimfix = gi.cvar("aimfix", "0", CVAR_ARCHIVE);
|
||||
g_machinegun_norecoil = gi.cvar("g_machinegun_norecoil", "0", CVAR_ARCHIVE);
|
||||
g_swap_speed = gi.cvar("g_swap_speed", "1", 0);
|
||||
|
||||
/* items */
|
||||
InitItems();
|
||||
|
||||
Com_sprintf(game.helpmessage1, sizeof(game.helpmessage1), "");
|
||||
Com_sprintf(game.helpmessage2, sizeof(game.helpmessage2), "");
|
||||
|
||||
/* initialize all entities for this game */
|
||||
game.maxentities = maxentities->value;
|
||||
g_edicts = gi.TagMalloc(game.maxentities * sizeof(g_edicts[0]), TAG_GAME);
|
||||
globals.edicts = g_edicts;
|
||||
globals.max_edicts = game.maxentities;
|
||||
|
||||
/* initialize all clients for this game */
|
||||
game.maxclients = maxclients->value;
|
||||
game.clients = gi.TagMalloc(game.maxclients * sizeof(game.clients[0]), TAG_GAME);
|
||||
globals.num_edicts = game.maxclients + 1;
|
||||
|
||||
CTFInit();
|
||||
}
|
||||
|
||||
/* ========================================================= */
|
||||
|
||||
void
|
||||
WriteField1(FILE *f, field_t *field, byte *base)
|
||||
{
|
||||
void *p;
|
||||
int len;
|
||||
int index;
|
||||
|
||||
p = (void *)(base + field->ofs);
|
||||
|
||||
switch (field->type)
|
||||
{
|
||||
case F_INT:
|
||||
case F_FLOAT:
|
||||
case F_ANGLEHACK:
|
||||
case F_VECTOR:
|
||||
case F_IGNORE:
|
||||
break;
|
||||
|
||||
case F_LSTRING:
|
||||
case F_GSTRING:
|
||||
|
||||
if (*(char **)p)
|
||||
{
|
||||
len = strlen(*(char **)p) + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
len = 0;
|
||||
}
|
||||
|
||||
*(int *)p = len;
|
||||
break;
|
||||
case F_EDICT:
|
||||
|
||||
if (*(edict_t **)p == NULL)
|
||||
{
|
||||
index = -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
index = *(edict_t **)p - g_edicts;
|
||||
}
|
||||
|
||||
*(int *)p = index;
|
||||
break;
|
||||
case F_CLIENT:
|
||||
|
||||
if (*(gclient_t **)p == NULL)
|
||||
{
|
||||
index = -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
index = *(gclient_t **)p - game.clients;
|
||||
}
|
||||
|
||||
*(int *)p = index;
|
||||
break;
|
||||
case F_ITEM:
|
||||
|
||||
if (*(edict_t **)p == NULL)
|
||||
{
|
||||
index = -1;
|
||||
}
|
||||
else
|
||||
{
|
||||
index = *(gitem_t **)p - itemlist;
|
||||
}
|
||||
|
||||
*(int *)p = index;
|
||||
break;
|
||||
|
||||
default:
|
||||
gi.error("WriteEdict: unknown field type");
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
WriteField2(FILE *f, field_t *field, byte *base)
|
||||
{
|
||||
int len;
|
||||
void *p;
|
||||
|
||||
p = (void *)(base + field->ofs);
|
||||
|
||||
switch (field->type)
|
||||
{
|
||||
case F_LSTRING:
|
||||
case F_GSTRING:
|
||||
|
||||
if (*(char **)p)
|
||||
{
|
||||
len = strlen(*(char **)p) + 1;
|
||||
fwrite(*(char **)p, len, 1, f);
|
||||
}
|
||||
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
ReadField(FILE *f, field_t *field, byte *base)
|
||||
{
|
||||
void *p;
|
||||
int len;
|
||||
int index;
|
||||
|
||||
p = (void *)(base + field->ofs);
|
||||
|
||||
switch (field->type)
|
||||
{
|
||||
case F_INT:
|
||||
case F_FLOAT:
|
||||
case F_ANGLEHACK:
|
||||
case F_VECTOR:
|
||||
case F_IGNORE:
|
||||
break;
|
||||
|
||||
case F_LSTRING:
|
||||
len = *(int *)p;
|
||||
|
||||
if (!len)
|
||||
{
|
||||
*(char **)p = NULL;
|
||||
}
|
||||
else
|
||||
{
|
||||
*(char **)p = gi.TagMalloc(len, TAG_LEVEL);
|
||||
fread(*(char **)p, len, 1, f);
|
||||
}
|
||||
|
||||
break;
|
||||
case F_GSTRING:
|
||||
len = *(int *)p;
|
||||
|
||||
if (!len)
|
||||
{
|
||||
*(char **)p = NULL;
|
||||
}
|
||||
else
|
||||
{
|
||||
*(char **)p = gi.TagMalloc(len, TAG_GAME);
|
||||
fread(*(char **)p, len, 1, f);
|
||||
}
|
||||
|
||||
break;
|
||||
case F_EDICT:
|
||||
index = *(int *)p;
|
||||
|
||||
if (index == -1)
|
||||
{
|
||||
*(edict_t **)p = NULL;
|
||||
}
|
||||
else
|
||||
{
|
||||
*(edict_t **)p = &g_edicts[index];
|
||||
}
|
||||
|
||||
break;
|
||||
case F_CLIENT:
|
||||
index = *(int *)p;
|
||||
|
||||
if (index == -1)
|
||||
{
|
||||
*(gclient_t **)p = NULL;
|
||||
}
|
||||
else
|
||||
{
|
||||
*(gclient_t **)p = &game.clients[index];
|
||||
}
|
||||
|
||||
break;
|
||||
case F_ITEM:
|
||||
index = *(int *)p;
|
||||
|
||||
if (index == -1)
|
||||
{
|
||||
*(gitem_t **)p = NULL;
|
||||
}
|
||||
else
|
||||
{
|
||||
*(gitem_t **)p = &itemlist[index];
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default:
|
||||
gi.error("ReadEdict: unknown field type");
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================================= */
|
||||
|
||||
/*
|
||||
* All pointer variables (except function
|
||||
* pointers) must be handled specially.
|
||||
*/
|
||||
void
|
||||
WriteClient(FILE *f, gclient_t *client)
|
||||
{
|
||||
field_t *field;
|
||||
gclient_t temp;
|
||||
|
||||
/* all of the ints, floats, and vectors stay as they are */
|
||||
temp = *client;
|
||||
|
||||
/* change the pointers to lengths or indexes */
|
||||
for (field = clientfields; field->name; field++)
|
||||
{
|
||||
WriteField1(f, field, (byte *)&temp);
|
||||
}
|
||||
|
||||
/* write the block */
|
||||
fwrite(&temp, sizeof(temp), 1, f);
|
||||
|
||||
/* now write any allocated data following the edict */
|
||||
for (field = clientfields; field->name; field++)
|
||||
{
|
||||
WriteField2(f, field, (byte *)client);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* All pointer variables (except function
|
||||
* pointers) must be handled specially.
|
||||
*/
|
||||
void
|
||||
ReadClient(FILE *f, gclient_t *client, short save_ver )
|
||||
{
|
||||
field_t *field;
|
||||
|
||||
fread(client, sizeof(*client), 1, f);
|
||||
|
||||
for (field = clientfields; field->name; field++)
|
||||
{
|
||||
ReadField(f, field, (byte *)client);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* This will be called whenever the game goes to a new level,
|
||||
* and when the user explicitly saves the game.
|
||||
*
|
||||
* Game information include cross level data, like multi level
|
||||
* triggers, help computer info, and all client states.
|
||||
*
|
||||
* A single player death will automatically restore from the
|
||||
* last save position.
|
||||
*/
|
||||
void
|
||||
WriteGame(const char *filename, qboolean autosave)
|
||||
{
|
||||
FILE *f;
|
||||
int i;
|
||||
char str[16];
|
||||
|
||||
if (!autosave)
|
||||
{
|
||||
SaveClientData();
|
||||
}
|
||||
|
||||
f = fopen(filename, "wb");
|
||||
|
||||
if (!f)
|
||||
{
|
||||
gi.error("Couldn't open %s", filename);
|
||||
}
|
||||
|
||||
memset(str, 0, sizeof(str));
|
||||
strcpy(str, BUILD_DATE);
|
||||
fwrite(str, sizeof(str), 1, f);
|
||||
|
||||
game.autosaved = autosave;
|
||||
fwrite(&game, sizeof(game), 1, f);
|
||||
game.autosaved = false;
|
||||
|
||||
for (i = 0; i < game.maxclients; i++)
|
||||
{
|
||||
WriteClient(f, &game.clients[i]);
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
void
|
||||
ReadGame(const char *filename)
|
||||
{
|
||||
FILE *f;
|
||||
int i;
|
||||
char str[16];
|
||||
|
||||
gi.FreeTags(TAG_GAME);
|
||||
|
||||
f = fopen(filename, "rb");
|
||||
|
||||
if (!f)
|
||||
{
|
||||
gi.error("Couldn't open %s", filename);
|
||||
}
|
||||
|
||||
fread(str, sizeof(str), 1, f);
|
||||
|
||||
if (strcmp(str, BUILD_DATE))
|
||||
{
|
||||
fclose(f);
|
||||
gi.error("Savegame from an older version.\n");
|
||||
}
|
||||
|
||||
g_edicts = gi.TagMalloc(game.maxentities * sizeof(g_edicts[0]), TAG_GAME);
|
||||
globals.edicts = g_edicts;
|
||||
|
||||
fread(&game, sizeof(game), 1, f);
|
||||
game.clients = gi.TagMalloc(game.maxclients * sizeof(game.clients[0]), TAG_GAME);
|
||||
|
||||
for (i = 0; i < game.maxclients; i++)
|
||||
{
|
||||
ReadClient(f, &game.clients[i], 0);
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
/* ========================================================== */
|
||||
|
||||
/*
|
||||
* All pointer variables (except function
|
||||
* pointers) must be handled specially.
|
||||
*/
|
||||
void
|
||||
WriteEdict(FILE *f, edict_t *ent)
|
||||
{
|
||||
field_t *field;
|
||||
edict_t temp;
|
||||
|
||||
/* all of the ints, floats, and vectors stay as they are */
|
||||
temp = *ent;
|
||||
|
||||
/* change the pointers to lengths or indexes */
|
||||
for (field = savefields; field->name; field++)
|
||||
{
|
||||
WriteField1(f, field, (byte *)&temp);
|
||||
}
|
||||
|
||||
/* write the block */
|
||||
fwrite(&temp, sizeof(temp), 1, f);
|
||||
|
||||
/* now write any allocated data following the edict */
|
||||
for (field = savefields; field->name; field++)
|
||||
{
|
||||
WriteField2(f, field, (byte *)ent);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* All pointer variables (except function
|
||||
* pointers) must be handled specially.
|
||||
*/
|
||||
void
|
||||
WriteLevelLocals(FILE *f)
|
||||
{
|
||||
field_t *field;
|
||||
level_locals_t temp;
|
||||
|
||||
/* all of the ints, floats, and vectors stay as they are */
|
||||
temp = level;
|
||||
|
||||
/* change the pointers to lengths or indexes */
|
||||
for (field = levelfields; field->name; field++)
|
||||
{
|
||||
WriteField1(f, field, (byte *)&temp);
|
||||
}
|
||||
|
||||
/* write the block */
|
||||
fwrite(&temp, sizeof(temp), 1, f);
|
||||
|
||||
/* now write any allocated data following the edict */
|
||||
for (field = levelfields; field->name; field++)
|
||||
{
|
||||
WriteField2(f, field, (byte *)&level);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* All pointer variables (except function
|
||||
* pointers) must be handled specially.
|
||||
*/
|
||||
void
|
||||
ReadEdict(FILE *f, edict_t *ent)
|
||||
{
|
||||
field_t *field;
|
||||
|
||||
fread(ent, sizeof(*ent), 1, f);
|
||||
|
||||
for (field = savefields; field->name; field++)
|
||||
{
|
||||
ReadField(f, field, (byte *)ent);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* All pointer variables (except function
|
||||
* pointers) must be handled specially.
|
||||
*/
|
||||
void
|
||||
ReadLevelLocals(FILE *f)
|
||||
{
|
||||
field_t *field;
|
||||
|
||||
fread(&level, sizeof(level), 1, f);
|
||||
|
||||
for (field = levelfields; field->name; field++)
|
||||
{
|
||||
ReadField(f, field, (byte *)&level);
|
||||
}
|
||||
}
|
||||
|
||||
void
|
||||
WriteLevel(const char *filename)
|
||||
{
|
||||
int i;
|
||||
edict_t *ent;
|
||||
FILE *f;
|
||||
void *base;
|
||||
|
||||
f = fopen(filename, "wb");
|
||||
|
||||
if (!f)
|
||||
{
|
||||
gi.error("Couldn't open %s", filename);
|
||||
}
|
||||
|
||||
/* write out edict size for checking */
|
||||
i = sizeof(edict_t);
|
||||
fwrite(&i, sizeof(i), 1, f);
|
||||
|
||||
/* write out a function pointer for checking */
|
||||
base = (void *)InitGame;
|
||||
fwrite(&base, sizeof(base), 1, f);
|
||||
|
||||
/* write out level_locals_t */
|
||||
WriteLevelLocals(f);
|
||||
|
||||
/* write out all the entities */
|
||||
for (i = 0; i < globals.num_edicts; i++)
|
||||
{
|
||||
ent = &g_edicts[i];
|
||||
|
||||
if (!ent->inuse)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
fwrite(&i, sizeof(i), 1, f);
|
||||
WriteEdict(f, ent);
|
||||
}
|
||||
|
||||
i = -1;
|
||||
fwrite(&i, sizeof(i), 1, f);
|
||||
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
/*
|
||||
* SpawnEntities will allready have been called on the
|
||||
* level the same way it was when the level was saved.
|
||||
*
|
||||
* That is necessary to get the baselines
|
||||
* set up identically.
|
||||
*
|
||||
* The server will have cleared all of the world links before
|
||||
* calling ReadLevel.
|
||||
*
|
||||
* No clients are connected yet.
|
||||
*/
|
||||
void
|
||||
ReadLevel(const char *filename)
|
||||
{
|
||||
int entnum;
|
||||
FILE *f;
|
||||
int i;
|
||||
void *base;
|
||||
edict_t *ent;
|
||||
|
||||
f = fopen(filename, "rb");
|
||||
|
||||
if (!f)
|
||||
{
|
||||
gi.error("Couldn't open %s", filename);
|
||||
}
|
||||
|
||||
/* free any dynamic memory allocated by
|
||||
loading the level base state */
|
||||
gi.FreeTags(TAG_LEVEL);
|
||||
|
||||
/* wipe all the entities */
|
||||
memset(g_edicts, 0, game.maxentities * sizeof(g_edicts[0]));
|
||||
globals.num_edicts = maxclients->value + 1;
|
||||
|
||||
/* check edict size */
|
||||
fread(&i, sizeof(i), 1, f);
|
||||
|
||||
if (i != sizeof(edict_t))
|
||||
{
|
||||
fclose(f);
|
||||
gi.error("ReadLevel: mismatched edict size");
|
||||
}
|
||||
|
||||
/* check function pointer base address */
|
||||
fread(&base, sizeof(base), 1, f);
|
||||
|
||||
if (base != (void *)InitGame)
|
||||
{
|
||||
fclose(f);
|
||||
gi.error("ReadLevel: function pointers have moved");
|
||||
}
|
||||
|
||||
/* load the level locals */
|
||||
ReadLevelLocals(f);
|
||||
|
||||
/* load all the entities */
|
||||
while (1)
|
||||
{
|
||||
if (fread(&entnum, sizeof(entnum), 1, f) != 1)
|
||||
{
|
||||
fclose(f);
|
||||
gi.error("ReadLevel: failed to read entnum");
|
||||
}
|
||||
|
||||
if (entnum == -1)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (entnum >= globals.num_edicts)
|
||||
{
|
||||
globals.num_edicts = entnum + 1;
|
||||
}
|
||||
|
||||
ent = &g_edicts[entnum];
|
||||
ReadEdict(f, ent);
|
||||
|
||||
/* let the server rebuild world links for this ent */
|
||||
memset(&ent->area, 0, sizeof(ent->area));
|
||||
gi.linkentity(ent);
|
||||
}
|
||||
|
||||
fclose(f);
|
||||
|
||||
/* mark all clients as unconnected */
|
||||
for (i = 0; i < maxclients->value; i++)
|
||||
{
|
||||
ent = &g_edicts[i + 1];
|
||||
ent->client = game.clients + i;
|
||||
ent->client->pers.connected = false;
|
||||
}
|
||||
|
||||
/* do any load time things at this point */
|
||||
for (i = 0; i < globals.num_edicts; i++)
|
||||
{
|
||||
ent = &g_edicts[i];
|
||||
|
||||
if (!ent->inuse)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
/* fire any cross-level triggers */
|
||||
if (ent->classname)
|
||||
{
|
||||
if (strcmp(ent->classname, "target_crosslevel_target") == 0)
|
||||
{
|
||||
ent->nextthink = level.time + ent->delay;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
@ -1050,6 +1050,7 @@ player_die(edict_t *self, edict_t *inflictor, edict_t *attacker,
|
|||
self->movetype = MOVETYPE_TOSS;
|
||||
|
||||
self->s.modelindex2 = 0; /* remove linked weapon model */
|
||||
self->s.modelindex3 = 0; /* remove linked ctf flag */
|
||||
|
||||
self->s.angles[0] = 0;
|
||||
self->s.angles[2] = 0;
|
||||
|
@ -1069,7 +1070,22 @@ player_die(edict_t *self, edict_t *inflictor, edict_t *attacker,
|
|||
ClientObituary(self, inflictor, attacker);
|
||||
TossClientWeapon(self);
|
||||
|
||||
if (deathmatch->value)
|
||||
/* if at start and same team, clear */
|
||||
if (ctf->value && (meansOfDeath == MOD_TELEFRAG) &&
|
||||
(self->client->resp.ctf_state < 2) &&
|
||||
(self->client->resp.ctf_team == attacker->client->resp.ctf_team))
|
||||
{
|
||||
attacker->client->resp.score--;
|
||||
self->client->resp.ctf_state = 0;
|
||||
}
|
||||
|
||||
CTFFragBonuses(self, inflictor, attacker);
|
||||
TossClientWeapon(self);
|
||||
CTFPlayerResetGrapple(self);
|
||||
CTFDeadDropFlag(self);
|
||||
CTFDeadDropTech(self);
|
||||
|
||||
if (deathmatch->value && !self->client->showscores)
|
||||
{
|
||||
Cmd_Help_f(self); /* show scores */
|
||||
}
|
||||
|
@ -1114,6 +1130,9 @@ player_die(edict_t *self, edict_t *inflictor, edict_t *attacker,
|
|||
sphere->die(sphere, self, self, 0, vec3_origin);
|
||||
}
|
||||
|
||||
/* clear inventory */
|
||||
memset(self->client->pers.inventory, 0, sizeof(self->client->pers.inventory));
|
||||
|
||||
/* if we've been killed by the tracker, GIB! */
|
||||
if ((meansOfDeath & ~MOD_FRIENDLY_FIRE) == MOD_TRACKER)
|
||||
{
|
||||
|
@ -1158,7 +1177,8 @@ player_die(edict_t *self, edict_t *inflictor, edict_t *attacker,
|
|||
|
||||
self->flags &= ~FL_NOGIB;
|
||||
ThrowClientHead(self, damage);
|
||||
|
||||
self->client->anim_priority = ANIM_DEATH;
|
||||
self->client->anim_end = 0;
|
||||
self->takedamage = DAMAGE_NO;
|
||||
}
|
||||
else
|
||||
|
@ -1235,6 +1255,10 @@ InitClientPersistant(gclient_t *client)
|
|||
client->pers.inventory[client->pers.selected_item] = 1;
|
||||
|
||||
client->pers.weapon = item;
|
||||
client->pers.lastweapon = item;
|
||||
|
||||
item = FindItem("Grapple");
|
||||
client->pers.inventory[ITEM_INDEX(item)] = 1;
|
||||
|
||||
client->pers.health = 100;
|
||||
client->pers.max_health = 100;
|
||||
|
@ -1264,9 +1288,21 @@ InitClientResp(gclient_t *client)
|
|||
return;
|
||||
}
|
||||
|
||||
int ctf_team = client->resp.ctf_team;
|
||||
qboolean id_state = client->resp.id_state;
|
||||
|
||||
memset(&client->resp, 0, sizeof(client->resp));
|
||||
|
||||
client->resp.ctf_team = ctf_team;
|
||||
client->resp.id_state = id_state;
|
||||
|
||||
client->resp.enterframe = level.framenum;
|
||||
client->resp.coop_respawn = client->pers;
|
||||
|
||||
if (ctf->value && (client->resp.ctf_team < CTF_TEAM1))
|
||||
{
|
||||
CTFAssignTeam(client);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -1675,9 +1711,16 @@ SelectSpawnPoint(edict_t *ent, vec3_t origin, vec3_t angles)
|
|||
}
|
||||
|
||||
if (deathmatch->value)
|
||||
{
|
||||
if (ctf->value)
|
||||
{
|
||||
spot = SelectCTFSpawnPoint(ent);
|
||||
}
|
||||
else
|
||||
{
|
||||
spot = SelectDeathmatchSpawnPoint();
|
||||
}
|
||||
}
|
||||
else if (coop->value)
|
||||
{
|
||||
spot = SelectCoopSpawnPoint(ent);
|
||||
|
@ -2050,12 +2093,23 @@ PutClientInServer(edict_t *ent)
|
|||
}
|
||||
else if (coop->value)
|
||||
{
|
||||
int n;
|
||||
char userinfo[MAX_INFO_STRING];
|
||||
|
||||
resp = client->resp;
|
||||
memcpy(userinfo, client->pers.userinfo, sizeof(userinfo));
|
||||
resp.coop_respawn.game_helpchanged = client->pers.game_helpchanged;
|
||||
resp.coop_respawn.helpchanged = client->pers.helpchanged;
|
||||
|
||||
/* this is kind of ugly, but it's how we want to handle keys in coop */
|
||||
for (n = 0; n < MAX_ITEMS; n++)
|
||||
{
|
||||
if (itemlist[n].flags & IT_KEY)
|
||||
{
|
||||
resp.coop_respawn.inventory[n] = client->pers.inventory[n];
|
||||
}
|
||||
}
|
||||
|
||||
client->pers = resp.coop_respawn;
|
||||
ClientUserinfoChanged(ent, userinfo);
|
||||
|
||||
|
@ -2103,7 +2157,7 @@ PutClientInServer(edict_t *ent)
|
|||
ent->waterlevel = 0;
|
||||
ent->watertype = 0;
|
||||
ent->flags &= ~FL_NO_KNOCKBACK;
|
||||
ent->svflags = 0;
|
||||
ent->svflags &= ~SVF_DEADMONSTER;
|
||||
|
||||
VectorCopy(mins, ent->mins);
|
||||
VectorCopy(maxs, ent->maxs);
|
||||
|
@ -2115,6 +2169,7 @@ PutClientInServer(edict_t *ent)
|
|||
client->ps.pmove.origin[0] = spawn_origin[0] * 8;
|
||||
client->ps.pmove.origin[1] = spawn_origin[1] * 8;
|
||||
client->ps.pmove.origin[2] = spawn_origin[2] * 8;
|
||||
client->ps.pmove.pm_flags &= ~PMF_NO_PREDICTION;
|
||||
|
||||
if (deathmatch->value && ((int)dmflags->value & DF_FIXED_FOV))
|
||||
{
|
||||
|
@ -2146,6 +2201,7 @@ PutClientInServer(edict_t *ent)
|
|||
|
||||
/* clear entity state values */
|
||||
ent->s.effects = 0;
|
||||
ent->s.skinnum = ent - g_edicts - 1;
|
||||
ent->s.modelindex = 255; /* will use the skin specified model */
|
||||
ent->s.modelindex2 = 255; /* custom gun model */
|
||||
|
||||
|
@ -2171,6 +2227,11 @@ PutClientInServer(edict_t *ent)
|
|||
VectorCopy(ent->s.angles, client->ps.viewangles);
|
||||
VectorCopy(ent->s.angles, client->v_angle);
|
||||
|
||||
if (CTFStartClient(ent))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/* spawn a spectator */
|
||||
if (client->pers.spectator)
|
||||
{
|
||||
|
@ -2376,8 +2437,18 @@ ClientUserinfoChanged(edict_t *ent, char *userinfo)
|
|||
playernum = ent - g_edicts - 1;
|
||||
|
||||
/* combine name and skin into a configstring */
|
||||
if (ctf->value)
|
||||
{
|
||||
CTFAssignSkin(ent, s);
|
||||
}
|
||||
else
|
||||
{
|
||||
gi.configstring(CS_PLAYERSKINS + playernum,
|
||||
va("%s\\%s", ent->client->pers.netname, s));
|
||||
}
|
||||
|
||||
/* set player name field (used in id_state view) */
|
||||
gi.configstring(CS_GENERAL + playernum, ent->client->pers.netname);
|
||||
|
||||
/* fov */
|
||||
if (deathmatch->value && ((int)dmflags->value & DF_FIXED_FOV))
|
||||
|
@ -2490,6 +2561,9 @@ ClientConnect(edict_t *ent, char *userinfo)
|
|||
if (ent->inuse == false)
|
||||
{
|
||||
/* clear the respawning variables */
|
||||
ent->client->resp.ctf_team = -1;
|
||||
ent->client->resp.id_state = true;
|
||||
|
||||
InitClientResp(ent->client);
|
||||
|
||||
if (!game.autosaved || !ent->client->pers.weapon)
|
||||
|
@ -2531,6 +2605,12 @@ ClientDisconnect(edict_t *ent)
|
|||
|
||||
gi.bprintf(PRINT_HIGH, "%s disconnected\n", ent->client->pers.netname);
|
||||
|
||||
if (ctf->value)
|
||||
{
|
||||
CTFDeadDropFlag(ent);
|
||||
CTFDeadDropTech(ent);
|
||||
}
|
||||
|
||||
/* make sure no trackers are still hurting us. */
|
||||
if (ent->client->tracker_pain_framenum)
|
||||
{
|
||||
|
@ -2770,6 +2850,11 @@ ClientThink(edict_t *ent, usercmd_t *ucmd)
|
|||
VectorCopy(pm.viewangles, client->ps.viewangles);
|
||||
}
|
||||
|
||||
if (client->ctf_grapple)
|
||||
{
|
||||
CTFGrapplePull(client->ctf_grapple);
|
||||
}
|
||||
|
||||
gi.linkentity(ent);
|
||||
|
||||
ent->gravity = 1.0;
|
||||
|
@ -2815,7 +2900,8 @@ ClientThink(edict_t *ent, usercmd_t *ucmd)
|
|||
ent->light_level = ucmd->lightlevel;
|
||||
|
||||
/* fire weapon from final position if needed */
|
||||
if (client->latched_buttons & BUTTON_ATTACK)
|
||||
if (client->latched_buttons & BUTTON_ATTACK
|
||||
&& (ent->movetype != MOVETYPE_NOCLIP))
|
||||
{
|
||||
if (client->resp.spectator)
|
||||
{
|
||||
|
@ -2838,6 +2924,12 @@ ClientThink(edict_t *ent, usercmd_t *ucmd)
|
|||
}
|
||||
}
|
||||
|
||||
if (ctf->value)
|
||||
{
|
||||
/* regen tech */
|
||||
CTFApplyRegeneration(ent);
|
||||
}
|
||||
|
||||
if (client->resp.spectator)
|
||||
{
|
||||
if (ucmd->upmove >= 10)
|
||||
|
@ -2872,6 +2964,14 @@ ClientThink(edict_t *ent, usercmd_t *ucmd)
|
|||
UpdateChaseCam(other);
|
||||
}
|
||||
}
|
||||
|
||||
if (ctf->value && client->menudirty && (client->menutime <= level.time))
|
||||
{
|
||||
PMenu_Do_Update(ent);
|
||||
gi.unicast(ent, true);
|
||||
client->menutime = level.time;
|
||||
client->menudirty = false;
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
|
@ -2906,7 +3006,8 @@ ClientBeginServerFrame(edict_t *ent)
|
|||
}
|
||||
|
||||
/* run weapon animations if it hasn't been done by a ucmd_t */
|
||||
if (!client->weapon_thunk && !client->resp.spectator)
|
||||
if (!client->weapon_thunk && !client->resp.spectator
|
||||
&& (ent->movetype != MOVETYPE_NOCLIP))
|
||||
{
|
||||
Think_Weapon(ent);
|
||||
}
|
||||
|
@ -2931,7 +3032,9 @@ ClientBeginServerFrame(edict_t *ent)
|
|||
}
|
||||
|
||||
if ((client->latched_buttons & buttonMask) ||
|
||||
(deathmatch->value && ((int)dmflags->value & DF_FORCE_RESPAWN)))
|
||||
(deathmatch->value &&
|
||||
((int)dmflags->value & DF_FORCE_RESPAWN)) ||
|
||||
CTFMatchOn())
|
||||
{
|
||||
respawn(ent);
|
||||
client->latched_buttons = 0;
|
||||
|
|
|
@ -772,6 +772,16 @@ P_FallingDamage(edict_t *ent)
|
|||
|
||||
delta = delta * delta * 0.0001;
|
||||
|
||||
/* never take damage if just release grapple or on grapple */
|
||||
if (ctf->value && (
|
||||
(level.time - ent->client->ctf_grapplereleasetime <= FRAMETIME * 2) ||
|
||||
(ent->client->ctf_grapple &&
|
||||
(ent->client->ctf_grapplestate > CTF_GRAPPLE_STATE_FLY))
|
||||
))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
/* never take falling damage if completely underwater */
|
||||
if (ent->waterlevel == 3)
|
||||
{
|
||||
|
@ -1104,13 +1114,18 @@ G_SetClientEffects(edict_t *ent)
|
|||
}
|
||||
}
|
||||
|
||||
if (ctf->value)
|
||||
{
|
||||
CTFEffects(ent);
|
||||
}
|
||||
|
||||
if (ent->client->quad_framenum > level.framenum)
|
||||
{
|
||||
remaining = ent->client->quad_framenum - level.framenum;
|
||||
|
||||
if ((remaining > 30) || (remaining & 4))
|
||||
{
|
||||
ent->s.effects |= EF_QUAD;
|
||||
CTFSetPowerUpEffect(ent, EF_QUAD);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1151,7 +1166,7 @@ G_SetClientEffects(edict_t *ent)
|
|||
|
||||
if ((remaining > 30) || (remaining & 4))
|
||||
{
|
||||
ent->s.effects |= EF_PENT;
|
||||
CTFSetPowerUpEffect(ent, EF_PENT);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1363,6 +1378,15 @@ newanim:
|
|||
client->anim_run = run;
|
||||
|
||||
if (!ent->groundentity)
|
||||
{
|
||||
/* if on grapple, don't go into jump
|
||||
frame, go into standing frame */
|
||||
if (client->ctf_grapple)
|
||||
{
|
||||
ent->s.frame = FRAME_stand01;
|
||||
client->anim_end = FRAME_stand40;
|
||||
}
|
||||
else
|
||||
{
|
||||
client->anim_priority = ANIM_JUMP;
|
||||
|
||||
|
@ -1373,6 +1397,7 @@ newanim:
|
|||
|
||||
client->anim_end = FRAME_jump2;
|
||||
}
|
||||
}
|
||||
else if (run)
|
||||
{
|
||||
/* running */
|
||||
|
@ -1522,11 +1547,27 @@ ClientEndServerFrame(edict_t *ent)
|
|||
{
|
||||
G_SetSpectatorStats(ent);
|
||||
}
|
||||
else
|
||||
else if (!ent->client->chase_target)
|
||||
{
|
||||
G_SetStats(ent);
|
||||
}
|
||||
|
||||
/* update chasecam follower stats */
|
||||
for (i = 1; i <= maxclients->value; i++)
|
||||
{
|
||||
edict_t *e = g_edicts + i;
|
||||
|
||||
if (!e->inuse || (e->client->chase_target != ent))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
memcpy(e->client->ps.stats, ent->client->ps.stats,
|
||||
sizeof(ent->client->ps.stats));
|
||||
e->client->ps.stats[STAT_LAYOUTS] = 1;
|
||||
break;
|
||||
}
|
||||
|
||||
G_CheckChaseStats(ent);
|
||||
G_SetClientEvent(ent);
|
||||
G_SetClientEffects(ent);
|
||||
|
@ -1544,8 +1585,19 @@ ClientEndServerFrame(edict_t *ent)
|
|||
{
|
||||
/* if the scoreboard is up, update it */
|
||||
if (ent->client->showscores)
|
||||
{
|
||||
|
||||
if (ent->client->menu)
|
||||
{
|
||||
PMenu_Do_Update(ent);
|
||||
ent->client->menudirty = false;
|
||||
ent->client->menutime = level.time;
|
||||
}
|
||||
else
|
||||
{
|
||||
DeathmatchScoreboardMessage(ent, ent->enemy);
|
||||
}
|
||||
|
||||
gi.unicast(ent, false);
|
||||
}
|
||||
|
||||
|
|
|
@ -266,8 +266,8 @@ InitGame(void)
|
|||
/* items */
|
||||
InitItems();
|
||||
|
||||
game.helpmessage1[0] = 0;
|
||||
game.helpmessage2[0] = 0;
|
||||
Com_sprintf(game.helpmessage1, sizeof(game.helpmessage1), "");
|
||||
Com_sprintf(game.helpmessage2, sizeof(game.helpmessage2), "");
|
||||
|
||||
/* initialize all entities for this game */
|
||||
game.maxentities = maxentities->value;
|
||||
|
|
Loading…
Reference in a new issue