Towards 1.13
#11
I don't quite understand what you mean altogether. Are you proposing that we add another translation for the BlockState, so that the data is represented:
1. In BlockArea: A single block is represented by a single number, index into per-BlockArea Palette
2. In Palette: A single index maps to "BlockTypeName" and "BlockStateIndex", an index into a global BlockStatePalette
3. In BlockStatePalette: A single index corresponds to the entire BlockState representation (map of key -> value)

In code:
class Palette
{
  struct BlockDef
  {
    std::string mBlockTypeName;  // such as "minecraft:acacia_door"
    size_t mBlockStateIndex;  // Index into global BlockStatePalette
  };
  std::map<UInt32, BlockDef> mBlockTypeMap;
};

using BlockState = map<std::string, std::string>;  // For simplicity, only consider strings as values for now

class BlockStatePalette
{
  std::map<size_t, BlockState> mBlockStateMap;
};

// global:
BlockStatePalette gBlockStatePalette;

class BlockArea
{
  UInt32 mBlocks[];  // To be replaced by smaller numbers when possible
  Palette mPalette;
  
  std::pair<const AString &, const BlockState &> block(const Vector3i & aPos)
  {
    UInt32 paletteIndex = mBlocks[...];
    const AString & blockTypeName = mPalette.mBlockTypeMap[paletteIndex].mBlockTypeName;
    const BlockState & blockState = gPalette.mBlockStateMap[mPalette.mBlockTypeMap[paletteIndex].mBlockStateIndex];
    return {blockTypeName, blockState};
  }
};
I hope the code for BlockArea::block() illustrates the concept enough.

Anyway, such a proposal is possible to implement later on as an optimization, and I don't think the added complexity is worth the effort and savings. I've done a bit of calculations, here's the raw math.

A chunk section (16*16*16 blocks) in the current pre-1.13 format uses exactly 8 KiB of RAM (16 * 16 * 16 blocks, each block 12 bits of type and 4 bits of meta)
A chunk section in the 1.13 format with a local palette can use from 16 KiB of RAM through 2 KiB of RAM downto 0.5 KiB of RAM (16 * 16 * 16 blocks, each block either 32-bit, 4-bit or 1-bit; exact bit-size is governed by the amount of different block types). So we could end up with up to twice as much RAM requirement in the worst case, but also 16 times less RAM in the best case. Considering the 0.5 KiB, storing the BlockStates as a string -> string map could mean a lot of memory, but there will only be 2 blocks in such a case anyway, so not too much of a BlockState to store. In the 16 KiB size, there could be up to 4K different blocks, but my opinion is that many of those will not have any BlockState at all, so the memory used will be reasonable as well.
Basically, I don't suppose that changing the internal representation to one with the local palette would change the RAM requirements dramatically.
Reply
Thanks given by:
#12
I've been thinking about the block type registry. What information we want to store for blocks, and what we don't want to store / hardcode.

While peaking at cBlockInfo, I found the following "type properties":
- LightValue (how much light the block emits at night)
- SpreadLightFalloff (how much light is lost when it goes through this block)
- IsTransparent
- IsOneHitDig
- IsPistonBreakable
- IsRainBlocker
- IsSkylightDispersant
- IsSnowable
- IsSolid
- IsUseableBySpectator
- FullyOccupiesVoxel
- CanBeTerraformed (will the terrain generator overwrite such a block)

Some of these would be better represented at the respective simulators - the Snowfall simulator should work with a list of block which support snow on their top, the piston pushing should be handled by the piston simulator (could be a separate one from the redstone simulator itself). Terraforming should be handled by the generator rather than the block type itself. Still, for new blocks, especially those plugin-registered, it would make sense to have a hint for the simulators or generators what to do.

Others may depend on some further properties - the LightValue of a redstone lamp depends on whether it's turned on or off, and that's only in its BlockState, not by a separate BlockType. These would be best represented by callback function - "How much light does this BlockType with this BlockState give out?"

So I guess the best structure to support all this is a free-form map of string->string for "hints" and a list of optional callbacks:
class BlockInfo
{
  AString mTypeName;
  std::map<AString, AString> mHints;
  std::function<UInt8(const AString & aTypeName, const BlockState & aBlockState)> mLightValueCallback;
  std::function<...> mXYZCallback;//  ...
}

The BlockTypeRegistry is then basically a map of the BlockTypeName to BlockInfo structures, with some extra housekeeping.
Reply
Thanks given by:
#13
Another one which would require a callback is 'IsRainBlocker'. This is needed for trapdoors.

Quote:So I guess the best structure to support all this is a free-form map of string->string for "hints" ..

I Assume the key is the name of the block, but what would be the value?
Reply
Thanks given by:
#14
(01-22-2019, 06:56 PM)NiLSPACE Wrote: I Assume the key is the name of the block, but what would be the value?

Nope. The map would be something like "IsSnowable" -> "1", "IsPistonBreakable" -> "0", "PipeVolume" -> "100" (there's a fluid-oriented mod out there, isn't there?). A separate map for each BlockType.

The evaluation should go something like:
1. If there's a callback, use it
2. No callback? Look for the hint
3. No hint either? Take a guess
Reply
Thanks given by:
#15
Ah, I see. Perhaps we shouldn't hardcode the callbacks though but save them just like the hints, in a map. Just in case a plugin wants to do something dynamically.
Reply
Thanks given by:
#16
Also, wouldn't it be better to use an enum as a key?
Reply
Thanks given by:
#17
It might be better for performance, but it's not good for extensibility (see the "PipeVolume" example in my previous reply). Since this map will not be accessed too much in a performance-critical code path, I think the string key is a good choice.

I think I have a pretty good skeleton for the registry, I'm just finishing it up and then will put it up for review.
Reply
Thanks given by:
#18
PR with skeleton code and rather extensive tests ( = examples):
https://github.com/cuberite/cuberite/pull/4310
Reply
Thanks given by: NiLSPACE
#19
Late to the party here but I had some ideas that I thought might be worth sharing.

The idea of a global palette is mentioned as just an optimisation but I think it might be a key part of the initial 1.13 implementation.  IMO the easiest path to 1.13 support is to keep supporting the current block id + meta data pattern used throughout the game logic. This could allow the chunk representation to be changed for 1.13 support without needing to rewrite half of the game logic first.

My idea for a global block palette/registry is that when you register a new block you also specify how many unique block states are required. The palette then assigns two things:
  • A BlockId that is just a unique number for that block name
  • A contiguous range of BlockStateId values on the global palette which uniquely represent block name + block state
This is how I envisage it working:
  • The chunk local palette only stores a single index into the global palette, not name + index as your example shows.
  • The "meta value" is still just dumb data like BlockMeta is currently and won't require any new data structures.
  • The "meta value" can be calculated directly from the BlockStateId
             BlockStateMeta = BlockStateId - (Lowest BlockStateId for this block type)
    So it's really just a State index that's specific to that block type.
  • When you need to access individual values from the block state it could be handled by the respective block handler. I pass in the meta value and it returns a struct with the components or alternatively just have query functions like many block handlers have currently.
In code:

enum class BlockId : UInt16 {};        // The new BlockType
enum class BlockStateId : UInt32 {};   // Global Id specifies both Type & Meta 
enum class BlockStateMeta : UInt16 {}; // State index for a known block type

class BlockPalette
{
public:

    struct BlockData
    {
        BlockId m_Id;
        BlockStateMeta m_State;
    };

    BlockId GetBlockId(const AString & a_BlockName) const;
    BlockId GetBlockId(BlockStateId a_StateId) const;

    BlockData GetBlockData(BlockStateId a_StateId) const;

    BlockId RegisterBlock(const AString & a_BlockName, UInt16 a_NumStates);

private:
    std::vector<BlockStateId> m_BlockIdToStateId;
    std::vector<AString> m_BlockIdToName;
    std::unordered_map<AString, BlockId> m_NameToBlockId;

};

BlockPalette gBlockStatePalette;

using ChunkPalette = std::map<UInt32, BlockStateId> mBlockStateMap;

class BlockArea
{
    UInt32 mBlocks[];  // To be replaced by smaller numbers when possible
    ChunkPalette mPalette;

    BlockData block(const Vector3i & aPos)
    {
        const UInt32 paletteIndex = mBlocks[...];
        const BlockStateId StateId = mPalette.mBlockStateMap[paletteIndex];
        return gPalette.GetBlockData(StateId);
    }
};

I don't know if I've explained it well but in my mind this seems far more than just an optimisation.
Reply
Thanks given by:
#20
I took the liberty and edited your post to use the shcode tag instead of the code tag, so that the code renders nicely Smile

(01-23-2019, 09:49 AM)peterbell10 Wrote: [...] you also specify how many unique block states are required [...]

And that's the main problem of your solution - for some blocks this number is really huge.
Reply
Thanks given by:




Users browsing this thread: 6 Guest(s)