mirror of
https://github.com/KartKrewDev/RingRacers.git
synced 2025-10-30 08:01:28 +00:00
Try some things to reduce rng biases
- Don't clamp all RNG calls to [0, FRACUNIT-1]. Only does this for P_RandomFixed now. - Use rejection sampling for any clamped RNG calls, to remove modulo bias. - Because of this, P_RandomRange ranges >= FRACUNIT are no longer undefined behavior. - Added P_Random/M_Random to grab RNG output directly. - Shuffle M_Random's RNG as well, since OS rand can be [0,INT32_MAX] instead of [0,UINT32_MAX].
This commit is contained in:
parent
df968e1b1a
commit
492c068cdd
4 changed files with 110 additions and 45 deletions
|
|
@ -402,10 +402,7 @@ static int lib_pRandomByte(lua_State *L)
|
||||||
static int lib_pRandomKey(lua_State *L)
|
static int lib_pRandomKey(lua_State *L)
|
||||||
{
|
{
|
||||||
INT32 a = (INT32)luaL_checkinteger(L, 1);
|
INT32 a = (INT32)luaL_checkinteger(L, 1);
|
||||||
|
|
||||||
NOHUD
|
NOHUD
|
||||||
if (a > 65536)
|
|
||||||
LUA_UsageWarning(L, "P_RandomKey: range > 65536 is undefined behavior");
|
|
||||||
lua_pushinteger(L, P_RandomKey(PR_UNDEFINED, a));
|
lua_pushinteger(L, P_RandomKey(PR_UNDEFINED, a));
|
||||||
demo_writerng = 2;
|
demo_writerng = 2;
|
||||||
return 1;
|
return 1;
|
||||||
|
|
@ -415,15 +412,13 @@ static int lib_pRandomRange(lua_State *L)
|
||||||
{
|
{
|
||||||
INT32 a = (INT32)luaL_checkinteger(L, 1);
|
INT32 a = (INT32)luaL_checkinteger(L, 1);
|
||||||
INT32 b = (INT32)luaL_checkinteger(L, 2);
|
INT32 b = (INT32)luaL_checkinteger(L, 2);
|
||||||
|
|
||||||
NOHUD
|
NOHUD
|
||||||
if (b < a) {
|
if (b < a)
|
||||||
|
{
|
||||||
INT32 c = a;
|
INT32 c = a;
|
||||||
a = b;
|
a = b;
|
||||||
b = c;
|
b = c;
|
||||||
}
|
}
|
||||||
if ((b-a+1) > 65536)
|
|
||||||
LUA_UsageWarning(L, "P_RandomRange: range > 65536 is undefined behavior");
|
|
||||||
lua_pushinteger(L, P_RandomRange(PR_UNDEFINED, a, b));
|
lua_pushinteger(L, P_RandomRange(PR_UNDEFINED, a, b));
|
||||||
demo_writerng = 2;
|
demo_writerng = 2;
|
||||||
return 1;
|
return 1;
|
||||||
|
|
|
||||||
126
src/m_random.c
126
src/m_random.c
|
|
@ -25,20 +25,56 @@
|
||||||
// RNG functions (not synched)
|
// RNG functions (not synched)
|
||||||
// ---------------------------
|
// ---------------------------
|
||||||
|
|
||||||
|
ATTRINLINE static UINT32 FUNCINLINE __external_prng__(void)
|
||||||
|
{
|
||||||
|
UINT32 rnd = rand();
|
||||||
|
|
||||||
|
#if RAND_MAX < 65535
|
||||||
|
// Compensate for especially bad randomness.
|
||||||
|
UINT32 rndv = (rand() & 1) << 15;
|
||||||
|
rnd ^= rndv;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Shuffle like we do for our own PRNG, since RAND_MAX
|
||||||
|
// tends to be [0, INT32_MAX] instead of [0, UINT32_MAX].
|
||||||
|
rnd ^= rnd >> 13;
|
||||||
|
rnd ^= rnd >> 11;
|
||||||
|
rnd ^= rnd << 21;
|
||||||
|
return (rnd * 36548569);
|
||||||
|
}
|
||||||
|
|
||||||
|
ATTRINLINE static UINT32 FUNCINLINE __external_prng_bound__(UINT32 bound)
|
||||||
|
{
|
||||||
|
// Do rejection sampling to remove the modulo bias.
|
||||||
|
UINT32 threshold = -bound % bound;
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
UINT32 r = __external_prng__();
|
||||||
|
if (r >= threshold)
|
||||||
|
{
|
||||||
|
return r % bound;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Provides a random 32-bit number. Distribution is uniform.
|
||||||
|
* As with all M_Random functions, not synched in netgames.
|
||||||
|
*
|
||||||
|
* \return A random 32-bit number.
|
||||||
|
*/
|
||||||
|
UINT32 M_Random(void)
|
||||||
|
{
|
||||||
|
return __external_prng__();
|
||||||
|
}
|
||||||
|
|
||||||
/** Provides a random fixed point number. Distribution is uniform.
|
/** Provides a random fixed point number. Distribution is uniform.
|
||||||
* As with all M_Random functions, not synched in netgames.
|
* As with all M_Random functions, not synched in netgames.
|
||||||
*
|
*
|
||||||
* \return A random fixed point number from [0,1).
|
* \return A random fixed point number from [0,1].
|
||||||
*/
|
*/
|
||||||
fixed_t M_RandomFixed(void)
|
fixed_t M_RandomFixed(void)
|
||||||
{
|
{
|
||||||
#if RAND_MAX < 65535
|
return (fixed_t)(__external_prng_bound__(FRACUNIT));
|
||||||
// Compensate for insufficient randomness.
|
|
||||||
fixed_t rndv = (rand()&1)<<15;
|
|
||||||
return rand()^rndv;
|
|
||||||
#else
|
|
||||||
return (rand() & 0xFFFF);
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Provides a random byte. Distribution is uniform.
|
/** Provides a random byte. Distribution is uniform.
|
||||||
|
|
@ -48,7 +84,7 @@ fixed_t M_RandomFixed(void)
|
||||||
*/
|
*/
|
||||||
UINT8 M_RandomByte(void)
|
UINT8 M_RandomByte(void)
|
||||||
{
|
{
|
||||||
return (rand() & 0xFF);
|
return (UINT8)(__external_prng_bound__(UINT8_MAX));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Provides a random integer for picking random elements from an array.
|
/** Provides a random integer for picking random elements from an array.
|
||||||
|
|
@ -58,9 +94,9 @@ UINT8 M_RandomByte(void)
|
||||||
* \param a Number of items in array.
|
* \param a Number of items in array.
|
||||||
* \return A random integer from [0,a).
|
* \return A random integer from [0,a).
|
||||||
*/
|
*/
|
||||||
INT32 M_RandomKey(INT32 a)
|
UINT32 M_RandomKey(UINT32 a)
|
||||||
{
|
{
|
||||||
return (INT32)((rand()/((unsigned)RAND_MAX+1.0f))*a);
|
return __external_prng_bound__(a);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Provides a random integer in a given range.
|
/** Provides a random integer in a given range.
|
||||||
|
|
@ -73,7 +109,7 @@ INT32 M_RandomKey(INT32 a)
|
||||||
*/
|
*/
|
||||||
INT32 M_RandomRange(INT32 a, INT32 b)
|
INT32 M_RandomRange(INT32 a, INT32 b)
|
||||||
{
|
{
|
||||||
return (INT32)((rand()/((unsigned)RAND_MAX+1.0f))*(b-a+1))+a;
|
return (INT32)(__external_prng_bound__((b - a) + 1)) + a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -92,23 +128,56 @@ typedef struct
|
||||||
|
|
||||||
static rng_t rng; // The entire PRNG state
|
static rng_t rng; // The entire PRNG state
|
||||||
|
|
||||||
/** Provides a random fixed point number.
|
/** Provides a random 32 bit integer.
|
||||||
* This is a variant of an xorshift PRNG; state fits in a 32 bit integer structure.
|
* This is a variant of an xorshift PRNG; state fits in a 32 bit integer structure.
|
||||||
*
|
*
|
||||||
* \return A random fixed point number from [0,1).
|
* \return A random, uniformly distributed number from [0,UINT32_MAX].
|
||||||
*/
|
*/
|
||||||
ATTRINLINE static fixed_t FUNCINLINE __internal_prng__(pr_class_t pr_class)
|
ATTRINLINE static UINT32 FUNCINLINE __internal_prng__(pr_class_t pr_class)
|
||||||
{
|
{
|
||||||
rng.seed[pr_class] ^= rng.seed[pr_class] >> 13;
|
rng.seed[pr_class] ^= rng.seed[pr_class] >> 13;
|
||||||
rng.seed[pr_class] ^= rng.seed[pr_class] >> 11;
|
rng.seed[pr_class] ^= rng.seed[pr_class] >> 11;
|
||||||
rng.seed[pr_class] ^= rng.seed[pr_class] << 21;
|
rng.seed[pr_class] ^= rng.seed[pr_class] << 21;
|
||||||
return ( (rng.seed[pr_class] * 36548569) >> 4) & (FRACUNIT-1);
|
return (rng.seed[pr_class] * 36548569);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Provides a random number within a specified range.
|
||||||
|
*
|
||||||
|
* \return A random, uniformly distributed number from [0,bound].
|
||||||
|
*/
|
||||||
|
ATTRINLINE static UINT32 FUNCINLINE __internal_prng_bound__(pr_class_t pr_class, UINT32 bound)
|
||||||
|
{
|
||||||
|
// Do rejection sampling to remove the modulo bias.
|
||||||
|
UINT32 threshold = -bound % bound;
|
||||||
|
for (;;)
|
||||||
|
{
|
||||||
|
UINT32 r = __internal_prng__(pr_class);
|
||||||
|
if (r >= threshold)
|
||||||
|
{
|
||||||
|
return r % bound;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Provides a random fixed point number. Distribution is uniform.
|
/** Provides a random fixed point number. Distribution is uniform.
|
||||||
* Literally a wrapper for the internal PRNG function.
|
* Literally a wrapper for the internal PRNG function.
|
||||||
*
|
*
|
||||||
* \return A random fixed point number from [0,1).
|
* \return A random fixed point number from [0,UINT32_MAX].
|
||||||
|
*/
|
||||||
|
#ifndef DEBUGRANDOM
|
||||||
|
UINT32 P_Random(pr_class_t pr_class)
|
||||||
|
{
|
||||||
|
#else
|
||||||
|
UINT32 P_RandomD(const char *rfile, INT32 rline, pr_class_t pr_class)
|
||||||
|
{
|
||||||
|
CONS_Printf("P_Random(%u) at: %sp %d\n", pr_class, rfile, rline);
|
||||||
|
#endif
|
||||||
|
return __internal_prng__(pr_class);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Provides a random fixed point number. Distribution is uniform.
|
||||||
|
*
|
||||||
|
* \return A random fixed point number from [0,1].
|
||||||
*/
|
*/
|
||||||
#ifndef DEBUGRANDOM
|
#ifndef DEBUGRANDOM
|
||||||
fixed_t P_RandomFixed(pr_class_t pr_class)
|
fixed_t P_RandomFixed(pr_class_t pr_class)
|
||||||
|
|
@ -118,14 +187,14 @@ fixed_t P_RandomFixedD(const char *rfile, INT32 rline, pr_class_t pr_class)
|
||||||
{
|
{
|
||||||
CONS_Printf("P_RandomFixed(%u) at: %sp %d\n", pr_class, rfile, rline);
|
CONS_Printf("P_RandomFixed(%u) at: %sp %d\n", pr_class, rfile, rline);
|
||||||
#endif
|
#endif
|
||||||
return __internal_prng__(pr_class);
|
return (fixed_t)(__internal_prng_bound__(pr_class, FRACUNIT));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Provides a random byte. Distribution is uniform.
|
/** Provides a random byte. Distribution is uniform.
|
||||||
* If you're curious, (&0xFF00) >> 8 gives the same result
|
* If you're curious, (&0xFF00) >> 8 gives the same result
|
||||||
* as a fixed point multiplication by 256.
|
* as a fixed point multiplication by 256.
|
||||||
*
|
*
|
||||||
* \return Random integer from [0, 255].
|
* \return Random integer from [0,255].
|
||||||
* \sa __internal_prng__
|
* \sa __internal_prng__
|
||||||
*/
|
*/
|
||||||
#ifndef DEBUGRANDOM
|
#ifndef DEBUGRANDOM
|
||||||
|
|
@ -136,7 +205,7 @@ UINT8 P_RandomByteD(const char *rfile, INT32 rline, pr_class_t pr_class)
|
||||||
{
|
{
|
||||||
CONS_Printf("P_RandomByte(%u) at: %sp %d\n", pr_class, rfile, rline);
|
CONS_Printf("P_RandomByte(%u) at: %sp %d\n", pr_class, rfile, rline);
|
||||||
#endif
|
#endif
|
||||||
return (UINT8)((__internal_prng__(pr_class) & 0xFF00) >> 8);
|
return (UINT8)(__internal_prng_bound__(pr_class, UINT8_MAX));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Provides a random integer for picking random elements from an array.
|
/** Provides a random integer for picking random elements from an array.
|
||||||
|
|
@ -144,23 +213,22 @@ UINT8 P_RandomByteD(const char *rfile, INT32 rline, pr_class_t pr_class)
|
||||||
* NOTE: Maximum range is 65536.
|
* NOTE: Maximum range is 65536.
|
||||||
*
|
*
|
||||||
* \param a Number of items in array.
|
* \param a Number of items in array.
|
||||||
* \return A random integer from [0,a).
|
* \return A random integer from [0,a].
|
||||||
* \sa __internal_prng__
|
* \sa __internal_prng__
|
||||||
*/
|
*/
|
||||||
#ifndef DEBUGRANDOM
|
#ifndef DEBUGRANDOM
|
||||||
INT32 P_RandomKey(pr_class_t pr_class, INT32 a)
|
UINT32 P_RandomKey(pr_class_t pr_class, UINT32 a)
|
||||||
{
|
{
|
||||||
#else
|
#else
|
||||||
INT32 P_RandomKeyD(const char *rfile, INT32 rline, pr_class_t pr_class, INT32 a)
|
UINT32 P_RandomKeyD(const char *rfile, INT32 rline, pr_class_t pr_class, UINT32 a)
|
||||||
{
|
{
|
||||||
CONS_Printf("P_RandomKey(%u) at: %sp %d\n", pr_class, rfile, rline);
|
CONS_Printf("P_RandomKey(%u) at: %sp %d\n", pr_class, rfile, rline);
|
||||||
#endif
|
#endif
|
||||||
return (INT32)(((INT64)__internal_prng__(pr_class) * a) >> FRACBITS);
|
return __internal_prng_bound__(pr_class, a);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Provides a random integer in a given range.
|
/** Provides a random integer in a given range.
|
||||||
* Distribution is uniform.
|
* Distribution is uniform.
|
||||||
* NOTE: Maximum range is 65536.
|
|
||||||
*
|
*
|
||||||
* \param a Lower bound.
|
* \param a Lower bound.
|
||||||
* \param b Upper bound.
|
* \param b Upper bound.
|
||||||
|
|
@ -175,7 +243,7 @@ INT32 P_RandomRangeD(const char *rfile, INT32 rline, pr_class_t pr_class, INT32
|
||||||
{
|
{
|
||||||
CONS_Printf("P_RandomRange(%u) at: %sp %d\n", pr_class, rfile, rline);
|
CONS_Printf("P_RandomRange(%u) at: %sp %d\n", pr_class, rfile, rline);
|
||||||
#endif
|
#endif
|
||||||
return (INT32)(((INT64)__internal_prng__(pr_class) * (b - a + 1)) >> FRACBITS) + a;
|
return (INT32)(__internal_prng_bound__(pr_class, (b - a) + 1)) + a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -187,13 +255,13 @@ INT32 P_RandomRangeD(const char *rfile, INT32 rline, pr_class_t pr_class, INT32
|
||||||
/** Peeks to see what the next result from the PRNG will be.
|
/** Peeks to see what the next result from the PRNG will be.
|
||||||
* Used for debugging.
|
* Used for debugging.
|
||||||
*
|
*
|
||||||
* \return A 'random' fixed point number from [0,1).
|
* \return A 'random' number from [0,UINT32_MAX]
|
||||||
* \sa __internal_prng__
|
* \sa __internal_prng__
|
||||||
*/
|
*/
|
||||||
fixed_t P_RandomPeek(pr_class_t pr_class)
|
UINT32 P_RandomPeek(pr_class_t pr_class)
|
||||||
{
|
{
|
||||||
UINT32 r = rng.seed[pr_class];
|
UINT32 r = rng.seed[pr_class];
|
||||||
fixed_t ret = __internal_prng__(pr_class);
|
UINT32 ret = __internal_prng__(pr_class);
|
||||||
rng.seed[pr_class] = r;
|
rng.seed[pr_class] = r;
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -69,37 +69,41 @@ typedef enum
|
||||||
// P_Random functions pulls random bytes from a PRNG that is network synced.
|
// P_Random functions pulls random bytes from a PRNG that is network synced.
|
||||||
|
|
||||||
// RNG functions
|
// RNG functions
|
||||||
|
UINT32 M_Random(void);
|
||||||
fixed_t M_RandomFixed(void);
|
fixed_t M_RandomFixed(void);
|
||||||
UINT8 M_RandomByte(void);
|
UINT8 M_RandomByte(void);
|
||||||
INT32 M_RandomKey(INT32 a);
|
UINT32 M_RandomKey(UINT32 a);
|
||||||
INT32 M_RandomRange(INT32 a, INT32 b);
|
INT32 M_RandomRange(INT32 a, INT32 b);
|
||||||
|
|
||||||
// PRNG functions
|
// PRNG functions
|
||||||
#ifdef DEBUGRANDOM
|
#ifdef DEBUGRANDOM
|
||||||
|
#define P_Random(c) P_RandomD(__FILE__, __LINE__, c)
|
||||||
#define P_RandomFixed(c) P_RandomFixedD(__FILE__, __LINE__, c)
|
#define P_RandomFixed(c) P_RandomFixedD(__FILE__, __LINE__, c)
|
||||||
#define P_RandomByte(c) P_RandomByteD(__FILE__, __LINE__, c)
|
#define P_RandomByte(c) P_RandomByteD(__FILE__, __LINE__, c)
|
||||||
#define P_RandomKey(c, d) P_RandomKeyD(__FILE__, __LINE__, c, d)
|
#define P_RandomKey(c, d) P_RandomKeyD(__FILE__, __LINE__, c, d)
|
||||||
#define P_RandomRange(c, d, e) P_RandomRangeD(__FILE__, __LINE__, c, d, e)
|
#define P_RandomRange(c, d, e) P_RandomRangeD(__FILE__, __LINE__, c, d, e)
|
||||||
|
UINT32 P_RandomD(const char *rfile, INT32 rline, pr_class_t pr_class);
|
||||||
fixed_t P_RandomFixedD(const char *rfile, INT32 rline, pr_class_t pr_class);
|
fixed_t P_RandomFixedD(const char *rfile, INT32 rline, pr_class_t pr_class);
|
||||||
UINT8 P_RandomByteD(const char *rfile, INT32 rline, pr_class_t pr_class);
|
UINT8 P_RandomByteD(const char *rfile, INT32 rline, pr_class_t pr_class);
|
||||||
INT32 P_RandomKeyD(const char *rfile, INT32 rline, pr_class_t pr_class, INT32 a);
|
UINT32 P_RandomKeyD(const char *rfile, INT32 rline, pr_class_t pr_class, UINT32 a);
|
||||||
INT32 P_RandomRangeD(const char *rfile, INT32 rline, pr_class_t pr_class, INT32 a, INT32 b);
|
INT32 P_RandomRangeD(const char *rfile, INT32 rline, pr_class_t pr_class, INT32 a, INT32 b);
|
||||||
#else
|
#else
|
||||||
|
UINT32 P_Random(pr_class_t pr_class);
|
||||||
fixed_t P_RandomFixed(pr_class_t pr_class);
|
fixed_t P_RandomFixed(pr_class_t pr_class);
|
||||||
UINT8 P_RandomByte(pr_class_t pr_class);
|
UINT8 P_RandomByte(pr_class_t pr_class);
|
||||||
INT32 P_RandomKey(pr_class_t pr_class, INT32 a);
|
UINT32 P_RandomKey(pr_class_t pr_class, UINT32 a);
|
||||||
INT32 P_RandomRange(pr_class_t pr_class, INT32 a, INT32 b);
|
INT32 P_RandomRange(pr_class_t pr_class, INT32 a, INT32 b);
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Macros for other functions
|
// Macros for other functions
|
||||||
#define M_SignedRandom() ((INT32)M_RandomByte() - 128) // [-128, 127] signed byte, originally a
|
#define M_SignedRandom() ((INT32)M_RandomByte() + INT8_MIN) // [-128, 127] signed byte, originally a
|
||||||
#define P_SignedRandom(pr) ((INT32)P_RandomByte(pr) - 128) // function of its own, moved to a macro
|
#define P_SignedRandom(pr) ((INT32)P_RandomByte(pr) + INT8_MIN) // function of its own, moved to a macro
|
||||||
|
|
||||||
#define M_RandomChance(p) (M_RandomFixed() < p) // given fixed point probability, p, between 0 (0%)
|
#define M_RandomChance(p) (M_RandomFixed() < p) // given fixed point probability, p, between 0 (0%)
|
||||||
#define P_RandomChance(pr, p) (P_RandomFixed(pr) < p) // and FRACUNIT (100%), returns true p% of the time
|
#define P_RandomChance(pr, p) (P_RandomFixed(pr) < p) // and FRACUNIT (100%), returns true p% of the time
|
||||||
|
|
||||||
// Debugging
|
// Debugging
|
||||||
fixed_t P_RandomPeek(pr_class_t pr_class);
|
UINT32 P_RandomPeek(pr_class_t pr_class);
|
||||||
|
|
||||||
// Working with the seed for PRNG
|
// Working with the seed for PRNG
|
||||||
#ifdef DEBUGRANDOM
|
#ifdef DEBUGRANDOM
|
||||||
|
|
|
||||||
|
|
@ -411,12 +411,10 @@ static void ST_drawDebugInfo(void)
|
||||||
// Figure out some other way to display all of the RNG classes.
|
// Figure out some other way to display all of the RNG classes.
|
||||||
|
|
||||||
fixed_t peekres = P_RandomPeek(PR_UNDEFINED);
|
fixed_t peekres = P_RandomPeek(PR_UNDEFINED);
|
||||||
peekres *= 10000; // Change from fixed point
|
|
||||||
peekres >>= FRACBITS; // to displayable decimal
|
|
||||||
|
|
||||||
V_DrawRightAlignedString(320, height - 16, V_MONOSPACE, va("Init: %08x", P_GetInitSeed(PR_UNDEFINED)));
|
V_DrawRightAlignedString(320, height - 16, V_MONOSPACE, va("Init: %08x", P_GetInitSeed(PR_UNDEFINED)));
|
||||||
V_DrawRightAlignedString(320, height - 8, V_MONOSPACE, va("Seed: %08x", P_GetRandSeed(PR_UNDEFINED)));
|
V_DrawRightAlignedString(320, height - 8, V_MONOSPACE, va("Seed: %08x", P_GetRandSeed(PR_UNDEFINED)));
|
||||||
V_DrawRightAlignedString(320, height, V_MONOSPACE, va("== : .%04d", peekres));
|
V_DrawRightAlignedString(320, height, V_MONOSPACE, va("== : %08x", peekres));
|
||||||
|
|
||||||
height -= 32;
|
height -= 32;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue