feat(composer): the structure roll -- 64 distinct stacks nobody wrote
Tier 4b. Until now the composer varied PARAMETERS within an archetype:
differently-sized caves. This rolls STRUCTURE, which is what makes a strate
a different KIND of place.
Per COMPOSER-NOTES §3.2: roll root polarity (rock+carve or void+fill) ->
a shape source legal for that polarity -> the conversion FOLLOWS from
polarity rather than being rolled -> k modifiers drawn from what the
CHANNEL DECLARATIONS say is legal, not from a hand-written list ->
structural posts appended automatically and not omittable.
64 distinct recipes of 64 (perfect structural diversity)
invalid recipes emitted: 0 (the roller cannot produce an
order the DAG validator rejects)
survival 46/64 (71.9%) non-vacuous 62, largest-share 60,
law pass 48
⭐ AND THEY ARE REAL PLACES NOBODY DESIGNED. Root polarity as a one-bit
identity lever, exactly as §3.2 predicted:
VF|r=vd/T|s=rg/T|x=fl/T TunnelNetwork's ROOM GRAPH rooted in void with
a FILL -- rooms become solid blobs hanging in
open space. air .978, feature scale 64.
VF|r=vd/M|s=lt/M|x=fl/M Maze's lattice corridors filled instead of
carved. air .938, feature scale 16.
RC|r=rk/V|s=sh/V|x=cv/V shafts carved from rock with a Maze modifier
set they never ship with.
Same parts, different worlds. That is the whole thesis of the op stack,
finally exercised.
⭐⭐ AND THE VERDICTS HOLD ON STACKS NOBODY VERIFIED:
489 proved (213 AllSolid, 276 AllAir), 2,071 mixed
650,859 voxels brute-forced
0 VIOLATIONS
This is what 8e346a3 was for. A false uniform verdict on an invented stack
means no geometry, no collision, and a player falls through the floor --
and it is the failure that cannot be seen without brute force. Zero.
Survival is 71.9% against 92.2% for the parameter roll, and that is the
EXPECTED direction, not a regression. Rolling structure aggressively is
supposed to fail sometimes; §3.5's reject-and-resample is what that is for.
A composer that only emits safe stacks is not exploring. Deliberately not
tuned.
Recipes are serialisable (op ids + param-block bindings), so a season
manifest can ship one -- which is also what will let an archetype become a
spatial REGION inside a strate rather than a label on it.
Build clean, 24 succeeded / 0 failed, verified by me in VoxelM.log.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VJiTcTSDNkKBtnNdiBtdaU
This commit is contained in:
1 parent
190ff381c0
commit
859a9b25c1
10 files changed
+2194
-55
No files matched your search
@@ -13,6 +13,7 @@
|
||||
|
||||
#include "VoxelSettings.h"
|
||||
#include "VoxelStrateDefinition.h"
|
||||
#include "VoxelDensityOpStack.h"
|
||||
|
||||
#include <type_traits>
|
||||
#include <utility>
|
||||
@@ -992,7 +993,8 @@ namespace
|
||||
}
|
||||
|
||||
FVoxelStrateRollInfo VF_RollInternal(const FVoxelStrateCorpus& Corpus,
|
||||
int32 Seed, int32 Index)
|
||||
int32 Seed, int32 Index,
|
||||
const ECaveGeneratorType* ForcedArchetype = nullptr)
|
||||
{
|
||||
FVoxelStrateRollInfo Result;
|
||||
if (!Corpus.IsValid())
|
||||
@@ -1043,7 +1045,24 @@ namespace
|
||||
}
|
||||
|
||||
int32 GroupIndex = 0;
|
||||
if (TotalGroupWeight > 0.0f)
|
||||
if (ForcedArchetype != nullptr)
|
||||
{
|
||||
GroupIndex = INDEX_NONE;
|
||||
for (int32 CandidateGroup = 0; CandidateGroup < Groups.Num(); ++CandidateGroup)
|
||||
{
|
||||
if (Groups[CandidateGroup].Archetype == *ForcedArchetype)
|
||||
{
|
||||
GroupIndex = CandidateGroup;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (GroupIndex == INDEX_NONE)
|
||||
{
|
||||
Result.FailureReason = TEXT("The requested archetype has no corpus group.");
|
||||
return Result;
|
||||
}
|
||||
}
|
||||
else if (TotalGroupWeight > 0.0f)
|
||||
{
|
||||
float Cursor = Rng.FRand() * TotalGroupWeight;
|
||||
for (int32 CandidateGroup = 0; CandidateGroup < Groups.Num(); ++CandidateGroup)
|
||||
@@ -1141,6 +1160,165 @@ namespace
|
||||
Result.bValid = true;
|
||||
return Result;
|
||||
}
|
||||
|
||||
struct FStructureModifierCandidate
|
||||
{
|
||||
EVoxelStrateOpClass OpClass;
|
||||
EVoxelStrateParamBlock ParamBlock;
|
||||
};
|
||||
|
||||
static const EVoxelStrateOpClass GStructureShapeSources[] =
|
||||
{
|
||||
EVoxelStrateOpClass::RoomGraphSource,
|
||||
EVoxelStrateOpClass::LatticeCorridorSource,
|
||||
EVoxelStrateOpClass::ShaftFieldSource,
|
||||
EVoxelStrateOpClass::IslandBlobSource,
|
||||
EVoxelStrateOpClass::NoiseRibbonSource,
|
||||
};
|
||||
|
||||
// Fixed order is intentional. This is a source-controlled catalogue, not a hash-container
|
||||
// iteration; changing it is a manifest-versioned design change rather than an accidental RNG
|
||||
// change.
|
||||
static const FStructureModifierCandidate GStructureModifiers[] =
|
||||
{
|
||||
{ EVoxelStrateOpClass::SdfRoughnessMod, EVoxelStrateParamBlock::None },
|
||||
{ EVoxelStrateOpClass::SdfCarve, EVoxelStrateParamBlock::None },
|
||||
{ EVoxelStrateOpClass::SdfFill, EVoxelStrateParamBlock::None },
|
||||
{ EVoxelStrateOpClass::CaveRoughnessMod, EVoxelStrateParamBlock::TunnelNetwork },
|
||||
{ EVoxelStrateOpClass::WormFieldSource, EVoxelStrateParamBlock::TunnelNetwork },
|
||||
{ EVoxelStrateOpClass::GridColumnMod, EVoxelStrateParamBlock::Slab },
|
||||
{ EVoxelStrateOpClass::DensityNoiseCarveMod, EVoxelStrateParamBlock::None },
|
||||
{ EVoxelStrateOpClass::DensityNoiseFillMod, EVoxelStrateParamBlock::None },
|
||||
{ EVoxelStrateOpClass::CaveTerraceMod, EVoxelStrateParamBlock::TunnelNetwork },
|
||||
{ EVoxelStrateOpClass::LayerLineMod, EVoxelStrateParamBlock::TunnelNetwork },
|
||||
{ EVoxelStrateOpClass::RibbingMod, EVoxelStrateParamBlock::TunnelNetwork },
|
||||
{ EVoxelStrateOpClass::CaveOverhangMod, EVoxelStrateParamBlock::TunnelNetwork },
|
||||
{ EVoxelStrateOpClass::CaveCliffMod, EVoxelStrateParamBlock::TunnelNetwork },
|
||||
{ EVoxelStrateOpClass::ScallopMod, EVoxelStrateParamBlock::TunnelNetwork },
|
||||
{ EVoxelStrateOpClass::CaveArchMod, EVoxelStrateParamBlock::TunnelNetwork },
|
||||
{ EVoxelStrateOpClass::RoomColumnMod, EVoxelStrateParamBlock::TunnelNetwork },
|
||||
{ EVoxelStrateOpClass::DomeMod, EVoxelStrateParamBlock::TunnelNetwork },
|
||||
{ EVoxelStrateOpClass::PinchMod, EVoxelStrateParamBlock::TunnelNetwork },
|
||||
{ EVoxelStrateOpClass::FloorBiasMod, EVoxelStrateParamBlock::TunnelNetwork },
|
||||
{ EVoxelStrateOpClass::ShaftLedgeMod, EVoxelStrateParamBlock::VerticalShaft },
|
||||
};
|
||||
|
||||
uint32 VF_StructureRollSeed(int32 Seed, int32 Index)
|
||||
{
|
||||
uint32 Value = VF_Avalanche(static_cast<uint32>(Seed) ^ 0xD1CEB00Bu);
|
||||
Value = VF_Avalanche(Value ^ (static_cast<uint32>(Index) + 0x7F4A7C15u));
|
||||
return Value;
|
||||
}
|
||||
|
||||
bool VF_IsContractLegalAfter(EVoxelOpChannelMask AvailableChannels,
|
||||
EVoxelOpResourceMask AvailableResources,
|
||||
const FVoxelStrateOpContract& Contract)
|
||||
{
|
||||
if ((Contract.Reads & static_cast<EVoxelOpChannelMask>(~AvailableChannels)) != 0
|
||||
|| (Contract.RequiredResources
|
||||
& static_cast<EVoxelOpResourceMask>(~AvailableResources)) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (Contract.bAdditive
|
||||
&& (Contract.Writes == VoxelOpChannels::None
|
||||
|| (Contract.Writes & static_cast<EVoxelOpChannelMask>(~Contract.Reads)) != 0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (const EVoxelOpChannelMask Channel :
|
||||
{ VoxelOpChannels::Density, VoxelOpChannels::Sdf })
|
||||
{
|
||||
if ((Contract.Writes & Channel) == 0 || (Contract.Reads & Channel) != 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
// All channels are already published by root + shape. A write-only candidate would
|
||||
// therefore be a replacement clobber and is excluded before it can enter a recipe.
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
FVoxelOpRecipeEntry VF_MakeRecipeEntry(EVoxelStrateOpClass OpClass,
|
||||
EVoxelStrateParamBlock ParamBlock)
|
||||
{
|
||||
FVoxelOpRecipeEntry Entry;
|
||||
Entry.OpClass = OpClass;
|
||||
Entry.ParamBlock = ParamBlock;
|
||||
return Entry;
|
||||
}
|
||||
|
||||
FVoxelOpStackRecipe VF_RollStructureInternal(int32 Seed, int32 Index)
|
||||
{
|
||||
FVoxelOpStackRecipe Recipe;
|
||||
FRandomStream Rng(static_cast<int32>(VF_StructureRollSeed(Seed, Index)));
|
||||
|
||||
Recipe.RootPolarity = Rng.RandRange(0, 1) == 0
|
||||
? EVoxelStrateRootPolarity::RockCarve
|
||||
: EVoxelStrateRootPolarity::VoidFill;
|
||||
|
||||
const EVoxelStrateOpClass ShapeClass = GStructureShapeSources[
|
||||
Rng.RandRange(0, UE_ARRAY_COUNT(GStructureShapeSources) - 1)];
|
||||
const EVoxelStrateParamBlock ShapeBlock =
|
||||
ShapeClass == EVoxelStrateOpClass::RoomGraphSource
|
||||
? EVoxelStrateParamBlock::TunnelNetwork
|
||||
: ShapeClass == EVoxelStrateOpClass::ShaftFieldSource
|
||||
? EVoxelStrateParamBlock::VerticalShaft
|
||||
: ShapeClass == EVoxelStrateOpClass::IslandBlobSource
|
||||
? EVoxelStrateParamBlock::FloatingIsland
|
||||
: EVoxelStrateParamBlock::Maze;
|
||||
|
||||
Recipe.Root = VF_MakeRecipeEntry(
|
||||
Recipe.RootPolarity == EVoxelStrateRootPolarity::VoidFill
|
||||
? EVoxelStrateOpClass::ConstantVoidSource
|
||||
: EVoxelStrateOpClass::ConstantRockSource,
|
||||
ShapeBlock);
|
||||
Recipe.ShapeSource = VF_MakeRecipeEntry(ShapeClass, ShapeBlock);
|
||||
Recipe.Conversion = VF_MakeRecipeEntry(
|
||||
Recipe.RootPolarity == EVoxelStrateRootPolarity::VoidFill
|
||||
? EVoxelStrateOpClass::SdfFill
|
||||
: EVoxelStrateOpClass::SdfCarve,
|
||||
ShapeBlock);
|
||||
Recipe.StructuralParamBlock = ShapeBlock;
|
||||
|
||||
FVoxelStrateOpContract ShapeContract;
|
||||
const bool bShapeDeclared = VoxelDensityOps::GetStrateOpContract(ShapeClass, ShapeContract);
|
||||
checkf(bShapeDeclared, TEXT("Structure catalogue contains an undeclared shape source."));
|
||||
|
||||
const EVoxelOpChannelMask AvailableChannels = VoxelOpChannels::Density | VoxelOpChannels::Sdf;
|
||||
const EVoxelOpResourceMask AvailableResources = ShapeContract.ProvidedResources;
|
||||
|
||||
TArray<FStructureModifierCandidate> Legal;
|
||||
for (const FStructureModifierCandidate& Candidate : GStructureModifiers)
|
||||
{
|
||||
FVoxelStrateOpContract Contract;
|
||||
if (!VoxelDensityOps::GetStrateOpContract(Candidate.OpClass, Contract))
|
||||
{
|
||||
checkf(false, TEXT("Structure catalogue contains an undeclared modifier."));
|
||||
continue;
|
||||
}
|
||||
if (VF_IsContractLegalAfter(AvailableChannels, AvailableResources, Contract))
|
||||
{
|
||||
Legal.Add(Candidate);
|
||||
}
|
||||
}
|
||||
|
||||
const int32 K = Rng.RandRange(4, 8);
|
||||
checkf(Legal.Num() >= K,
|
||||
TEXT("Structure roller has fewer legal modifiers than its rolled k."));
|
||||
for (int32 ModifierIndex = 0; ModifierIndex < K && Legal.Num() > 0; ++ModifierIndex)
|
||||
{
|
||||
const int32 Pick = Rng.RandRange(0, Legal.Num() - 1);
|
||||
const FStructureModifierCandidate Candidate = Legal[Pick];
|
||||
Recipe.Modifiers.Add(VF_MakeRecipeEntry(
|
||||
Candidate.OpClass,
|
||||
Candidate.ParamBlock == EVoxelStrateParamBlock::None
|
||||
? ShapeBlock : Candidate.ParamBlock));
|
||||
Legal.RemoveAt(Pick, 1, EAllowShrinking::No);
|
||||
}
|
||||
return Recipe;
|
||||
}
|
||||
}
|
||||
|
||||
const TArray<FVoxelStrateFieldExclusion>& FVoxelStrateCorpus::GetNonTunableFields()
|
||||
@@ -1849,8 +2027,164 @@ FVoxelStrateRollInfo VF_RollStrateParamsDetailed(const FVoxelStrateCorpus& Corpu
|
||||
return Result;
|
||||
}
|
||||
|
||||
FVoxelStrateRollInfo VF_RollStrateParamsDetailedForArchetype(
|
||||
const FVoxelStrateCorpus& Corpus, ECaveGeneratorType Archetype, int32 Seed, int32 Index)
|
||||
{
|
||||
const FVoxelStrateRollInfo Result = VF_RollInternal(Corpus, Seed, Index, &Archetype);
|
||||
|
||||
#if DO_CHECK
|
||||
if (Result.bValid)
|
||||
{
|
||||
const FVoxelStrateRollInfo Repeat = VF_RollInternal(Corpus, Seed, Index, &Archetype);
|
||||
checkf(Repeat.bValid && Repeat.Archetype == Result.Archetype
|
||||
&& VF_AreStrateArchetypeParamsBitIdentical(
|
||||
Repeat.ArchetypeParams, Result.ArchetypeParams, Result.Archetype)
|
||||
&& Repeat.ParentEntryIndices == Result.ParentEntryIndices
|
||||
&& Repeat.ParentWeights == Result.ParentWeights,
|
||||
TEXT("VF_RollStrateParamsDetailedForArchetype lost determinism."));
|
||||
}
|
||||
#endif
|
||||
return Result;
|
||||
}
|
||||
|
||||
FStrateGenerationParams VF_RollStrateParams(const FVoxelStrateCorpus& Corpus,
|
||||
int32 Seed, int32 Index)
|
||||
{
|
||||
return VF_RollStrateParamsDetailed(Corpus, Seed, Index).Params;
|
||||
}
|
||||
|
||||
namespace
|
||||
{
|
||||
const TCHAR* VF_RecipeOpCode(EVoxelStrateOpClass OpClass)
|
||||
{
|
||||
switch (OpClass)
|
||||
{
|
||||
case EVoxelStrateOpClass::ConstantRockSource: return TEXT("rk");
|
||||
case EVoxelStrateOpClass::ConstantVoidSource: return TEXT("vd");
|
||||
case EVoxelStrateOpClass::RoomGraphSource: return TEXT("rg");
|
||||
case EVoxelStrateOpClass::LatticeCorridorSource: return TEXT("lt");
|
||||
case EVoxelStrateOpClass::ShaftFieldSource: return TEXT("sh");
|
||||
case EVoxelStrateOpClass::IslandBlobSource: return TEXT("is");
|
||||
case EVoxelStrateOpClass::NoiseRibbonSource: return TEXT("nr");
|
||||
case EVoxelStrateOpClass::SdfRoughnessMod: return TEXT("sr");
|
||||
case EVoxelStrateOpClass::SdfCarve: return TEXT("cv");
|
||||
case EVoxelStrateOpClass::SdfFill: return TEXT("fl");
|
||||
case EVoxelStrateOpClass::GridColumnMod: return TEXT("gc");
|
||||
case EVoxelStrateOpClass::CaveRoughnessMod: return TEXT("cr");
|
||||
case EVoxelStrateOpClass::CaveTerraceMod: return TEXT("tr");
|
||||
case EVoxelStrateOpClass::LayerLineMod: return TEXT("ll");
|
||||
case EVoxelStrateOpClass::RibbingMod: return TEXT("rb");
|
||||
case EVoxelStrateOpClass::CaveOverhangMod: return TEXT("co");
|
||||
case EVoxelStrateOpClass::CaveCliffMod: return TEXT("cc");
|
||||
case EVoxelStrateOpClass::ScallopMod: return TEXT("sc");
|
||||
case EVoxelStrateOpClass::CaveArchMod: return TEXT("ar");
|
||||
case EVoxelStrateOpClass::RoomColumnMod: return TEXT("rc");
|
||||
case EVoxelStrateOpClass::DomeMod: return TEXT("dm");
|
||||
case EVoxelStrateOpClass::PinchMod: return TEXT("pn");
|
||||
case EVoxelStrateOpClass::FloorBiasMod: return TEXT("fb");
|
||||
case EVoxelStrateOpClass::WormFieldSource: return TEXT("wm");
|
||||
case EVoxelStrateOpClass::ShaftLedgeMod: return TEXT("sl");
|
||||
case EVoxelStrateOpClass::DensityNoiseCarveMod: return TEXT("nc");
|
||||
case EVoxelStrateOpClass::DensityNoiseFillMod: return TEXT("nf");
|
||||
default: return TEXT("??");
|
||||
}
|
||||
}
|
||||
|
||||
const TCHAR* VF_RecipeBlockCode(EVoxelStrateParamBlock Block)
|
||||
{
|
||||
switch (Block)
|
||||
{
|
||||
case EVoxelStrateParamBlock::TunnelNetwork: return TEXT("T");
|
||||
case EVoxelStrateParamBlock::Slab: return TEXT("B");
|
||||
case EVoxelStrateParamBlock::Maze: return TEXT("M");
|
||||
case EVoxelStrateParamBlock::Surface: return TEXT("S");
|
||||
case EVoxelStrateParamBlock::VerticalShaft: return TEXT("V");
|
||||
case EVoxelStrateParamBlock::FloatingIsland: return TEXT("I");
|
||||
default: return TEXT("-");
|
||||
}
|
||||
}
|
||||
|
||||
FString VF_FormatRecipeEntry(const FVoxelOpRecipeEntry& Entry)
|
||||
{
|
||||
return FString::Printf(TEXT("%s/%s"), VF_RecipeOpCode(Entry.OpClass),
|
||||
VF_RecipeBlockCode(Entry.ParamBlock));
|
||||
}
|
||||
}
|
||||
|
||||
FVoxelOpStackRecipe VF_RollStrateStructure(int32 Seed, int32 Index)
|
||||
{
|
||||
const FVoxelOpStackRecipe Recipe = VF_RollStructureInternal(Seed, Index);
|
||||
|
||||
#if DO_CHECK
|
||||
const FVoxelOpStackRecipe Repeat = VF_RollStructureInternal(Seed, Index);
|
||||
checkf(VF_AreStrateStructureRecipesIdentical(Recipe, Repeat),
|
||||
TEXT("VF_RollStrateStructure lost determinism for the same seed/index."));
|
||||
#endif
|
||||
return Recipe;
|
||||
}
|
||||
|
||||
FString VF_FormatStrateStructureRecipe(const FVoxelOpStackRecipe& Recipe)
|
||||
{
|
||||
FString Modifiers;
|
||||
for (int32 Index = 0; Index < Recipe.Modifiers.Num(); ++Index)
|
||||
{
|
||||
if (Index > 0) { Modifiers += TEXT(","); }
|
||||
Modifiers += VF_FormatRecipeEntry(Recipe.Modifiers[Index]);
|
||||
}
|
||||
return FString::Printf(TEXT("%s|r=%s|s=%s|x=%s|m=[%s]|posts=%s"),
|
||||
Recipe.RootPolarity == EVoxelStrateRootPolarity::VoidFill ? TEXT("VF") : TEXT("RC"),
|
||||
*VF_FormatRecipeEntry(Recipe.Root),
|
||||
*VF_FormatRecipeEntry(Recipe.ShapeSource),
|
||||
*VF_FormatRecipeEntry(Recipe.Conversion),
|
||||
*Modifiers,
|
||||
VF_RecipeBlockCode(Recipe.StructuralParamBlock));
|
||||
}
|
||||
|
||||
uint32 VF_HashStrateStructureRecipe(const FVoxelOpStackRecipe& Recipe)
|
||||
{
|
||||
uint32 Hash = 2166136261u;
|
||||
auto AddByte = [&Hash](uint8 Value)
|
||||
{
|
||||
Hash ^= static_cast<uint32>(Value);
|
||||
Hash *= 16777619u;
|
||||
};
|
||||
auto AddEntry = [&AddByte](const FVoxelOpRecipeEntry& Entry)
|
||||
{
|
||||
AddByte(static_cast<uint8>(Entry.OpClass));
|
||||
AddByte(static_cast<uint8>(Entry.ParamBlock));
|
||||
};
|
||||
|
||||
AddByte(static_cast<uint8>(Recipe.RootPolarity));
|
||||
AddEntry(Recipe.Root);
|
||||
AddEntry(Recipe.ShapeSource);
|
||||
AddEntry(Recipe.Conversion);
|
||||
AddByte(static_cast<uint8>(Recipe.Modifiers.Num()));
|
||||
for (const FVoxelOpRecipeEntry& Entry : Recipe.Modifiers) { AddEntry(Entry); }
|
||||
AddByte(static_cast<uint8>(Recipe.StructuralParamBlock));
|
||||
return Hash;
|
||||
}
|
||||
|
||||
bool VF_AreStrateStructureRecipesIdentical(const FVoxelOpStackRecipe& A,
|
||||
const FVoxelOpStackRecipe& B)
|
||||
{
|
||||
if (A.RootPolarity != B.RootPolarity
|
||||
|| A.Root.OpClass != B.Root.OpClass || A.Root.ParamBlock != B.Root.ParamBlock
|
||||
|| A.ShapeSource.OpClass != B.ShapeSource.OpClass
|
||||
|| A.ShapeSource.ParamBlock != B.ShapeSource.ParamBlock
|
||||
|| A.Conversion.OpClass != B.Conversion.OpClass
|
||||
|| A.Conversion.ParamBlock != B.Conversion.ParamBlock
|
||||
|| A.StructuralParamBlock != B.StructuralParamBlock
|
||||
|| A.Modifiers.Num() != B.Modifiers.Num())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (int32 Index = 0; Index < A.Modifiers.Num(); ++Index)
|
||||
{
|
||||
if (A.Modifiers[Index].OpClass != B.Modifiers[Index].OpClass
|
||||
|| A.Modifiers[Index].ParamBlock != B.Modifiers[Index].ParamBlock)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
Reference in new issue
Block a user