mirror of
https://github.com/nzp-team/fteqw.git
synced 2024-11-29 23:22:01 +00:00
2e1a70e319
maplist command now generates links. implemented skin objects for q3. added a csqc builtin for it. also supports compositing skins. playing demos inside zips/pk3s/paks should now work. bumped default rate cvar. added cl_transfer to attempt to connect to a new server without disconnecting first. rewrote fog command. alpha and mindist arguments are now supported. fog change also happens over a short time period. added new args to the showpic console command. can now create clickable items for touchscreen/absmouse users. fixed menus to properly support right-aligned text. this finally fixes variable-width fonts. rewrote console tab completion suggestions display. now clickable links. strings obtained from qc are now marked as const. this has required quite a few added consts all over the place. probably crappy attempt at adding joypad support to the sdl port. no idea if it works. changed key bind event code. buttons now track which event they should trigger when released, instead of being the same one the whole time. this allows +forward etc clickable buttons on screen. Also simplified modifier keys - they no longer trigger random events when pressing the modifier key itself. Right modifiers can now be bound separately from left modifiers. Right will use left's binding if not otherwise bound. Bind assumes left if there's no prefix. multiplayer->setup->network menu no longer crashes. added rgb colours to the translation view (but not to the colour-changing keys). added modelviewer command to view models. added menu_mods menu to switch mods in a more friendly way. will be shown by default if multiple manifests exist in the binarydir. clamped classic tracer density. scrag particles no longer look quite so buggy. added ifdefs to facilitate a potential winrt port. the engine should now have no extra dependencies, but still needs system code+audio drivers to be written. if it can't set a renderer, it'll now try to use *every* renderer until it finds one that works. added experimental mapcluster server mode (that console command). New maps will be started up as required. rewrote skeletal blending code a bit. added cylinder geomtypes. fix cfg_save writing to the wrong path bug. VFS_CLOSE now returns a boolean. false means there was some sort of fatal error (either crc when reading was bad, or the write got corrupted or something). Typically ignorable, depends how robust you want to be. win32 tls code now supports running as a server. added connect tls://address support, as well as equivalent sv_addport support. exposed basic model loading api to plugins. d3d11 backend now optionally supports tessellation hlsl. no suitable hlsl provided by default. !!tess to enable. attempted to add gamma ramp support for d3d11. added support for shader blobs to speed up load times. r_shaderblobs 1 to enable. almost vital for d3d11. added vid_srgb cvar. shadowless lights are no longer disabled if shadows are not supported. attempt to add support for touchscreens in win7/8. Wrote gimmicky lua support, using lua instead of ssqc. define VM_LUA to enable. updated saved game code. can again load saved games from vanilla-like engines. changed scale clamping. 0.0001 should no longer appear as 1. changed default mintic from 0.03 to 0.013 to match vanilla qw. I don't know why it was at 0.03. probably a typo. git-svn-id: https://svn.code.sf.net/p/fteqw/code/trunk@4623 fc73d0e0-1445-4013-8a0c-d673dee63da5
494 lines
14 KiB
C
494 lines
14 KiB
C
#define PROGSUSED
|
|
#include "progsint.h"
|
|
|
|
//#define MAPPING_DEBUG
|
|
//#define MAPPING_PARANOID //may actually break unions, so beware.
|
|
|
|
void PR_SetBuiltins(int type);
|
|
/*
|
|
progstate_t *pr_progstate;
|
|
progsnum_t pr_typecurrent;
|
|
int maxprogs;
|
|
|
|
progstate_t *current_progstate;
|
|
int numshares;
|
|
|
|
sharedvar_t *shares; //shared globals, not including parms
|
|
int maxshares;
|
|
*/
|
|
|
|
//switches progs without preserving parms/ret/shared
|
|
pbool PR_SwitchProgs(progfuncs_t *progfuncs, progsnum_t type)
|
|
{
|
|
if ((unsigned)type >= maxprogs)
|
|
{
|
|
if (type == -1)
|
|
{
|
|
pr_typecurrent = -1;
|
|
current_progstate = NULL;
|
|
return true;
|
|
}
|
|
PR_RunError(&progfuncs->funcs, "QCLIB: Bad prog type - %i", type);
|
|
// Sys_Error("Bad prog type - %i", type);
|
|
}
|
|
|
|
if (pr_progstate[(unsigned)type].progs == NULL) //we havn't loaded it yet, for some reason
|
|
return false;
|
|
|
|
current_progstate = &pr_progstate[(unsigned)type];
|
|
|
|
pr_typecurrent = type;
|
|
|
|
return true;
|
|
}
|
|
|
|
//switch to new progs, preserving all arguments. oldpr should be 'pr_typecurrent'
|
|
pbool PR_SwitchProgsParms(progfuncs_t *progfuncs, progsnum_t newpr) //from 2 to 1
|
|
{
|
|
unsigned int a;
|
|
progstate_t *np;
|
|
progstate_t *op;
|
|
int oldpr = pr_typecurrent;
|
|
|
|
if (newpr == oldpr)
|
|
{
|
|
//don't bother coping variables to themselves...
|
|
return true;
|
|
}
|
|
|
|
np = &pr_progstate[(int)newpr];
|
|
op = &pr_progstate[(int)oldpr];
|
|
|
|
if ((unsigned)newpr >= maxprogs || !np->globals)
|
|
{
|
|
printf("QCLIB: Bad prog type - %i", newpr);
|
|
return false;
|
|
}
|
|
if ((unsigned)oldpr >= maxprogs || !op->globals) //startup?
|
|
return PR_SwitchProgs(progfuncs, newpr);
|
|
|
|
//copy parms.
|
|
for (a = 0; a < MAX_PARMS;a++)
|
|
{
|
|
*(int *)&np->globals[OFS_PARM0+3*a ] = *(int *)&op->globals[OFS_PARM0+3*a ];
|
|
*(int *)&np->globals[OFS_PARM0+3*a+1] = *(int *)&op->globals[OFS_PARM0+3*a+1];
|
|
*(int *)&np->globals[OFS_PARM0+3*a+2] = *(int *)&op->globals[OFS_PARM0+3*a+2];
|
|
}
|
|
np->globals[OFS_RETURN] = op->globals[OFS_RETURN];
|
|
np->globals[OFS_RETURN+1] = op->globals[OFS_RETURN+1];
|
|
np->globals[OFS_RETURN+2] = op->globals[OFS_RETURN+2];
|
|
|
|
//move the vars defined as shared.
|
|
for (a = 0; a < numshares; a++)//fixme: make offset per progs
|
|
{
|
|
memmove(&((int *)np->globals)[shares[a].varofs], &((int *)op->globals)[shares[a].varofs], shares[a].size*4);
|
|
/* ((int *)p1->globals)[shares[a].varofs] = ((int *)p2->globals)[shares[a].varofs];
|
|
if (shares[a].size > 1)
|
|
{
|
|
((int *)p1->globals)[shares[a].varofs+1] = ((int *)p2->globals)[shares[a].varofs+1];
|
|
if (shares[a].size > 2)
|
|
((int *)p1->globals)[shares[a].varofs+2] = ((int *)p2->globals)[shares[a].varofs+2];
|
|
}
|
|
*/
|
|
}
|
|
return PR_SwitchProgs(progfuncs, newpr);
|
|
}
|
|
|
|
progsnum_t PDECL PR_LoadProgs(pubprogfuncs_t *ppf, const char *s, int headercrc, builtin_t *builtins, int numbuiltins)
|
|
{
|
|
progfuncs_t *progfuncs = (progfuncs_t*)ppf;
|
|
unsigned int a;
|
|
progsnum_t oldtype;
|
|
oldtype = pr_typecurrent;
|
|
for (a = 0; a < maxprogs; a++)
|
|
{
|
|
if (pr_progstate[a].progs == NULL)
|
|
{
|
|
pr_typecurrent = a;
|
|
current_progstate = &pr_progstate[a];
|
|
if (PR_ReallyLoadProgs(progfuncs, s, headercrc, &pr_progstate[a], false)) //try and load it
|
|
{
|
|
current_progstate->builtins = builtins;
|
|
current_progstate->numbuiltins = numbuiltins;
|
|
if (a <= progfuncs->funcs.numprogs)
|
|
progfuncs->funcs.numprogs = a+1;
|
|
|
|
#ifdef QCJIT
|
|
current_progstate->jit = PR_GenerateJit(progfuncs);
|
|
#endif
|
|
if (oldtype != -1)
|
|
PR_SwitchProgs(progfuncs, oldtype);
|
|
return a; //we could load it. Yay!
|
|
}
|
|
PR_SwitchProgs(progfuncs, oldtype);
|
|
return -1; // loading failed.
|
|
}
|
|
}
|
|
PR_SwitchProgs(progfuncs, oldtype);
|
|
return -1;
|
|
}
|
|
|
|
void PR_ShiftParms(progfuncs_t *progfuncs, int amount)
|
|
{
|
|
int a;
|
|
for (a = 0; a < MAX_PARMS - amount;a++)
|
|
*(int *)&pr_globals[OFS_PARM0+3*a] = *(int *)&pr_globals[OFS_PARM0+3*(amount+a)];
|
|
}
|
|
|
|
//forget a progs
|
|
void PR_Clear(progfuncs_t *progfuncs)
|
|
{
|
|
unsigned int a;
|
|
for (a = 0; a < maxprogs; a++)
|
|
{
|
|
#ifdef QCJIT
|
|
if (pr_progstate[a].jit)
|
|
PR_CloseJit(pr_progstate[a].jit);
|
|
#endif
|
|
pr_progstate[a].progs = NULL;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void QC_StartShares(progfuncs_t *progfuncs)
|
|
{
|
|
numshares = 0;
|
|
maxshares = 32;
|
|
if (shares)
|
|
externs->memfree(shares);
|
|
shares = externs->memalloc(sizeof(sharedvar_t)*maxshares);
|
|
}
|
|
void PDECL QC_AddSharedVar(pubprogfuncs_t *ppf, int start, int size) //fixme: make offset per progs and optional
|
|
{
|
|
progfuncs_t *progfuncs = (progfuncs_t*)ppf;
|
|
int ofs;
|
|
unsigned int a;
|
|
|
|
if (numshares >= maxshares)
|
|
{
|
|
void *buf;
|
|
buf = shares;
|
|
maxshares += 16;
|
|
shares = externs->memalloc(sizeof(sharedvar_t)*maxshares);
|
|
|
|
memcpy(shares, buf, sizeof(sharedvar_t)*numshares);
|
|
|
|
externs->memfree(buf);
|
|
}
|
|
ofs = start;
|
|
for (a = 0; a < numshares; a++)
|
|
{
|
|
if (shares[a].varofs+shares[a].size == ofs)
|
|
{
|
|
shares[a].size += size; //expand size.
|
|
return;
|
|
}
|
|
if (shares[a].varofs == start)
|
|
return;
|
|
}
|
|
|
|
|
|
shares[numshares].varofs = start;
|
|
shares[numshares].size = size;
|
|
numshares++;
|
|
}
|
|
|
|
|
|
//void ShowWatch(void);
|
|
|
|
void QC_InitShares(progfuncs_t *progfuncs)
|
|
{
|
|
// ShowWatch();
|
|
if (!prinst.field) //don't make it so we will just need to remalloc everything
|
|
{
|
|
prinst.maxfields = 64;
|
|
prinst.field = externs->memalloc(sizeof(fdef_t) * prinst.maxfields);
|
|
}
|
|
|
|
prinst.numfields = 0;
|
|
progfuncs->funcs.fieldadjust = 0;
|
|
}
|
|
|
|
void QC_FlushProgsOffsets(progfuncs_t *progfuncs)
|
|
{ //sets the fields up for loading a new progs.
|
|
//fields are matched by name to other progs
|
|
//not by offset
|
|
unsigned int i;
|
|
for (i = 0; i < prinst.numfields; i++)
|
|
prinst.field[i].progsofs = -1;
|
|
}
|
|
|
|
|
|
//called if a global is defined as a field
|
|
//returns offset.
|
|
|
|
//vectors must be added before any of their corresponding _x/y/z vars
|
|
//in this way, even screwed up progs work.
|
|
|
|
//requestedpos is the offset the engine WILL put it at.
|
|
//origionaloffs is used to track matching field offsets. fields with the same progs offset overlap
|
|
|
|
//note: we probably suffer from progs with renamed system globals.
|
|
int PDECL QC_RegisterFieldVar(pubprogfuncs_t *ppf, unsigned int type, char *name, signed long engineofs, signed long progsofs)
|
|
{
|
|
progfuncs_t *progfuncs = (progfuncs_t*)ppf;
|
|
// progstate_t *p;
|
|
// int pnum;
|
|
unsigned int i;
|
|
int namelen;
|
|
int ofs;
|
|
|
|
int fnum;
|
|
|
|
if (!name) //engine can use this to offset all progs fields
|
|
{ //which fixes constant field offsets (some ktpro arrays)
|
|
progfuncs->funcs.fieldadjust = fields_size/4;
|
|
#ifdef MAPPING_DEBUG
|
|
printf("FIELD ADJUST: %i %i %i\n", progfuncs->funcs.fieldadjust, fields_size, (int)fields_size/4);
|
|
#endif
|
|
return 0;
|
|
}
|
|
|
|
|
|
prinst.reorganisefields = true;
|
|
|
|
//look for an existing match
|
|
for (i = 0; i < prinst.numfields; i++)
|
|
{
|
|
if (!strcmp(name, prinst.field[i].name))
|
|
{
|
|
if (prinst.field[i].type != type)
|
|
{
|
|
/*Hexen2/DP compat hack: if the new type is a float and the original type is a vector, make the new def alias to the engine's _x field
|
|
this 'works around' the unused .vector color field used for rtlight colours vs the .float color used for particle colours (the float initialisers in map files will expand into the x slot safely).
|
|
qc/hc can work around this by just using .vector color/color_x instead, which is the same as this hack, but would resolve defs to allow rtlight colours.
|
|
*/
|
|
if (prinst.field[i].type != ev_vector || type != ev_float)
|
|
{
|
|
printf("Field type mismatch on \"%s\". %i != %i\n", name, prinst.field[i].type, type);
|
|
continue;
|
|
}
|
|
}
|
|
if (!progfuncs->funcs.fieldadjust && engineofs>=0)
|
|
if ((unsigned)engineofs/4 != prinst.field[i].ofs)
|
|
Sys_Error("Field %s at wrong offset", name);
|
|
|
|
if (prinst.field[i].progsofs == -1)
|
|
prinst.field[i].progsofs = progsofs;
|
|
#ifdef MAPPING_DEBUG
|
|
printf("Dupfield %s %i -> %i\n", name, prinst.field[i].progsofs,prinst.field[i].ofs);
|
|
#endif
|
|
return prinst.field[i].ofs-progfuncs->funcs.fieldadjust; //got a match
|
|
}
|
|
}
|
|
|
|
if (prinst.numfields+1>prinst.maxfields)
|
|
{
|
|
fdef_t *nf;
|
|
i = prinst.maxfields;
|
|
prinst.maxfields += 32;
|
|
nf = externs->memalloc(sizeof(fdef_t) * prinst.maxfields);
|
|
memcpy(nf, prinst.field, sizeof(fdef_t) * i);
|
|
externs->memfree(prinst.field);
|
|
prinst.field = nf;
|
|
}
|
|
|
|
//try to add a new one
|
|
fnum = prinst.numfields;
|
|
prinst.numfields++;
|
|
prinst.field[fnum].name = name;
|
|
prinst.field[fnum].type = type;
|
|
prinst.field[fnum].progsofs = progsofs;
|
|
if (engineofs >= 0)
|
|
{ //the engine is setting up a list of required field indexes.
|
|
|
|
//paranoid checking of the offset.
|
|
#if 0//def MAPPING_PARANOID
|
|
for (i = 0; i < numfields-1; i++)
|
|
{
|
|
if (field[i].ofs == ((unsigned)engineofs)/4)
|
|
{
|
|
if (type == ev_float && field[i].type == ev_vector) //check names
|
|
{
|
|
if (strncmp(field[i].name, name, strlen(field[i].name)))
|
|
Sys_Error("Duplicated offset");
|
|
}
|
|
else
|
|
Sys_Error("Duplicated offset");
|
|
}
|
|
}
|
|
#endif
|
|
if (engineofs&3)
|
|
Sys_Error("field %s is %i&3", name, (int)engineofs);
|
|
prinst.field[fnum].ofs = ofs = engineofs/4;
|
|
}
|
|
else
|
|
{ //we just found a new fieldname inside a progs
|
|
prinst.field[fnum].ofs = ofs = fields_size/4; //add on the end
|
|
|
|
//if the progs field offset matches annother offset in the same progs, make it match up with the earlier one.
|
|
if (progsofs>=0)
|
|
{
|
|
unsigned otherofs;
|
|
for (i = 0; i < prinst.numfields-1; i++)
|
|
{
|
|
otherofs = prinst.field[i].progsofs;
|
|
if (otherofs == -1) //qc unions work purely by progs offsets, not by engine offsets.
|
|
continue; //this is because there really is no reliable mapping between progs and engine, so don't get confused.
|
|
if (otherofs == (unsigned)progsofs)
|
|
{
|
|
#ifdef MAPPING_DEBUG
|
|
printf("union(%s) ", prinst.field[i].name);
|
|
#endif
|
|
prinst.field[fnum].ofs = ofs = prinst.field[i].ofs;
|
|
break;
|
|
}
|
|
if (prinst.field[i].type == ev_vector && otherofs+1 == (unsigned)progsofs)
|
|
{
|
|
#ifdef MAPPING_DEBUG
|
|
printf("union(%s) ", prinst.field[i].name);
|
|
#endif
|
|
prinst.field[fnum].ofs = ofs = prinst.field[i].ofs+1;
|
|
break;
|
|
}
|
|
if (prinst.field[i].type == ev_vector && otherofs+2 == (unsigned)progsofs)
|
|
{
|
|
#ifdef MAPPING_DEBUG
|
|
printf("union(%s) ", prinst.field[i].name);
|
|
#endif
|
|
prinst.field[fnum].ofs = ofs = prinst.field[i].ofs+2;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// if (type != ev_vector)
|
|
if (fields_size < (ofs+type_size[type])*4)
|
|
fields_size = (ofs+type_size[type])*4;
|
|
|
|
if (max_fields_size && fields_size > max_fields_size)
|
|
Sys_Error("Allocated too many additional fields after ents were inited.");
|
|
|
|
#ifdef MAPPING_DEBUG
|
|
printf("Field %s %i -> %i\n", name, prinst.field[fnum].progsofs,prinst.field[fnum].ofs);
|
|
#endif
|
|
|
|
if (type == ev_vector)
|
|
{
|
|
//vectors define float fields too. this avoids issues if the mod has (pointless) anti-decompile field reordering/stripping that works thanks to decompilers expecting things to be ordered exactly..
|
|
char *n;
|
|
namelen = strlen(name)+5;
|
|
|
|
n=PRHunkAlloc(progfuncs, namelen, "str");
|
|
sprintf(n, "%s_x", name);
|
|
ofs = QC_RegisterFieldVar(&progfuncs->funcs, ev_float, n, engineofs, progsofs);
|
|
|
|
n=PRHunkAlloc(progfuncs, namelen, "str");
|
|
sprintf(n, "%s_y", name);
|
|
QC_RegisterFieldVar(&progfuncs->funcs, ev_float, n, (engineofs==-1)?-1:(engineofs+4), (progsofs==-1)?-1:progsofs+1);
|
|
|
|
n=PRHunkAlloc(progfuncs, namelen, "str");
|
|
sprintf(n, "%s_z", name);
|
|
QC_RegisterFieldVar(&progfuncs->funcs, ev_float, n, (engineofs==-1)?-1:(engineofs+8), (progsofs==-1)?-1:progsofs+2);
|
|
}
|
|
|
|
//we've finished setting the structure
|
|
return ofs - progfuncs->funcs.fieldadjust;
|
|
}
|
|
|
|
|
|
//called if a global is defined as a field
|
|
void PDECL QC_AddSharedFieldVar(pubprogfuncs_t *ppf, int num, char *stringtable)
|
|
{
|
|
progfuncs_t *progfuncs = (progfuncs_t*)ppf;
|
|
// progstate_t *p;
|
|
// int pnum;
|
|
unsigned int i, o;
|
|
|
|
//look for an existing match not needed, cos we look a little later too.
|
|
/*
|
|
for (i = 0; i < numfields; i++)
|
|
{
|
|
if (!strcmp(pr_globaldefs[num].s_name, field[i].s_name))
|
|
{
|
|
//really we should look for a field def
|
|
|
|
*(int *)&pr_globals[pr_globaldefs[num].ofs] = field[i].ofs; //got a match
|
|
|
|
return;
|
|
}
|
|
}
|
|
*/
|
|
|
|
switch(current_progstate->structtype)
|
|
{
|
|
case PST_KKQWSV:
|
|
case PST_DEFAULT:
|
|
for (i=1 ; i<pr_progs->numfielddefs; i++)
|
|
{
|
|
if (!strcmp(pr_fielddefs16[i].s_name+stringtable, pr_globaldefs16[num].s_name+stringtable))
|
|
{
|
|
#ifdef MAPPING_DEBUG
|
|
int old = *(int *)&pr_globals[pr_globaldefs16[num].ofs];
|
|
#endif
|
|
*(int *)&pr_globals[pr_globaldefs16[num].ofs] = QC_RegisterFieldVar(&progfuncs->funcs, pr_fielddefs16[i].type, pr_globaldefs16[num].s_name+stringtable, -1, *(int *)&pr_globals[pr_globaldefs16[num].ofs]);
|
|
#ifdef MAPPING_DEBUG
|
|
printf("Field=%s global %i -> %i\n", pr_globaldefs16[num].s_name+stringtable, old, *(volatile int *)&pr_globals[pr_globaldefs16[num].ofs]);
|
|
#endif
|
|
return;
|
|
}
|
|
}
|
|
|
|
for (i = 0; i < prinst.numfields; i++)
|
|
{
|
|
o = prinst.field[i].progsofs;
|
|
if (o == *(unsigned int *)&pr_globals[pr_globaldefs16[num].ofs])
|
|
{
|
|
#ifdef MAPPING_DEBUG
|
|
int old = *(int *)&pr_globals[pr_globaldefs16[num].ofs];
|
|
#endif
|
|
*(int *)&pr_globals[pr_globaldefs16[num].ofs] = prinst.field[i].ofs-progfuncs->funcs.fieldadjust;
|
|
#ifdef MAPPING_DEBUG
|
|
printf("Field global=%s %i -> %i\n", pr_globaldefs16[num].s_name+stringtable, old, *(volatile int *)&pr_globals[pr_globaldefs16[num].ofs]);
|
|
#endif
|
|
return;
|
|
}
|
|
}
|
|
|
|
//oh well, must be a parameter.
|
|
// if (*(int *)&pr_globals[pr_globaldefs16[num].ofs])
|
|
// Sys_Error("QCLIB: Global field var with no matching field \"%s\", from offset %i", pr_globaldefs16[num].s_name+stringtable, *(int *)&pr_globals[pr_globaldefs16[num].ofs]);
|
|
return;
|
|
case PST_FTE32:
|
|
case PST_QTEST:
|
|
for (i=1 ; i<pr_progs->numfielddefs; i++)
|
|
{
|
|
if (!strcmp(pr_fielddefs32[i].s_name+stringtable, pr_globaldefs32[num].s_name+stringtable))
|
|
{
|
|
*(int *)&pr_globals[pr_globaldefs32[num].ofs] = QC_RegisterFieldVar(&progfuncs->funcs, pr_fielddefs32[i].type, pr_globaldefs32[num].s_name+stringtable, -1, *(int *)&pr_globals[pr_globaldefs32[num].ofs]);
|
|
return;
|
|
}
|
|
}
|
|
|
|
for (i = 0; i < prinst.numfields; i++)
|
|
{
|
|
o = prinst.field[i].progsofs;
|
|
if (o == *(unsigned int *)&pr_globals[pr_globaldefs32[num].ofs])
|
|
{
|
|
*(int *)&pr_globals[pr_globaldefs32[num].ofs] = prinst.field[i].ofs-progfuncs->funcs.fieldadjust;
|
|
return;
|
|
}
|
|
}
|
|
|
|
//oh well, must be a parameter.
|
|
if (*(int *)&pr_globals[pr_globaldefs32[num].ofs])
|
|
Sys_Error("QCLIB: Global field var with no matching field \"%s\", from offset %i", pr_globaldefs32[num].s_name+stringtable, *(int *)&pr_globals[pr_globaldefs32[num].ofs]);
|
|
return;
|
|
default:
|
|
Sys_Error("Bad bits");
|
|
break;
|
|
}
|
|
Sys_Error("Should be unreachable");
|
|
}
|
|
|