Backpacks with style.

Fixed SMP packet crash.
Fixed potential golem hook crash and added levels of interaction.
Added golem backpack skins.
This commit is contained in:
Chris committed 2016-01-23 16:59:56 -05:00
1 parent a43d6ae48c
commit 963dda76e1
25 files changed
+805 -280

No files matched your search

+8 -1
View File
@@ -1,8 +1,15 @@
01/22/2016 1.0.0.0 - Beta
01/23/2016 1.0.0.0 - Beta
Fixed SMP packet crash.
Fixed potential golem hook crash.
Added levels of interaction.
Added golem backpack skins.
01/22/2016 1.0.0.0 - Beta
Bumped version to 1.0.0.0
Fixes #213: Added backpack lore and recipe.
Added backpack config option.
Updated mod info file.
Fixed several SMP crashes.
01/21/2016 0.10.0.0 - Beta
Fixed multiple issues with the Arcane assembler: GUI desync, invalid network state.
@@ -52,6 +52,59 @@ import cpw.mods.fml.relauncher.SideOnly;
*/
public interface IGolemHookHandler
{
/**
* Defines interaction levels.
*
* @author Nividica
*
*/
public static enum InteractionLevel
{
/**
* The interaction was not handled at all.<br>
* <ul>
* <li>GUI will be shown, if golem has one.</li>
* <li>customInteraction() will not be called.</li>
* <li>Sync data will not be sent.</li>
* <li>setupGolem() will not be called.</li>
* </ul>
*/
NoInteraction,
/**
* The interaction was handled, but no sync data changed, and golem does not need to be re-setup.<br>
* <ul>
* <li>GUI will not be shown</li>
* <li>customInteraction() called.
* <li>
* <li>Sync data will not be sent.</li>
* <li>setupGolem() will not be called.</li>
* </ul>
*/
BasicInteraction,
/**
* The interaction was handled and sync data changed, but the golem does not need to be re-setup<br>
* <ul>
* <li>GUI will not be shown</li>
* <li>customInteraction() called.</li>
* <li>Sync data sent.</li>
* <li>setupGolem() will not be called.</li>
* </ul>
*/
SyncInteraction,
/**
* The interaction was handled and the golem needs to be re-setup.
* <ul>
* <li>GUI will not be shown</li>
* <li>customInteraction() called.</li>
* <li>Sync data sent.</li>
* <li>setupGolem() called.</li>
* </ul>
*/
FullInteraction;
}
/**
* Any data you wish to sync between the server and clients can be added here.<br/>
@@ -87,12 +140,11 @@ public interface IGolemHookHandler
* @param player
* @param side
* Server or Client
* @return True will cause customInteraction to be called.<br>
* If any handlers return true, the setupGolem method will be called once all handlers
* have finished the interaction.
* @return The level of interaction.
*/
public boolean canHandleInteraction( @Nonnull final EntityGolemBase golem, @Nullable Object handlerData, @Nonnull EntityPlayer player,
@Nonnull Side side );
@Nonnull
public InteractionLevel canHandleInteraction( @Nonnull final EntityGolemBase golem, @Nullable Object handlerData, @Nonnull EntityPlayer player,
@Nonnull Side side );
/**
* Called when the golem has been interacted with and it hasn't been handled by Thaumcraft.<br/>
@@ -100,17 +152,21 @@ public interface IGolemHookHandler
* @param golem
* @param handlerData
* Handler data attached to the golem.
* @param syncData
* @param player
* @param side
* Server or Client
* @return Return the handler data you wish to attach to the golem, or null to clear any attached data.
*/
@Nullable
public Object customInteraction( @Nonnull final EntityGolemBase golem, @Nullable Object handlerData, @Nonnull EntityPlayer player,
@Nonnull Side side );
public Object customInteraction( @Nonnull final EntityGolemBase golem, @Nullable Object handlerData, @Nonnull IGolemHookSyncRegistry syncData,
@Nonnull EntityPlayer player, @Nonnull Side side );
/**
* Called when a golem receives a tick, if {@code needsDynamicUpdate} returned true during registration.
* Called when a golem receives a tick, if {@code needsDynamicUpdate} returned true during registration.<br>
* If setting sync data, you do not need to track if the sync data has changed yourself. The {@code syncData} class
* tracks this internally and will only ever send updates if the data has actually changed. Although for performance reasons
* it would be a good idea to keep a tick counter and only set the data periodically if there is any processing involved.<br>
* Note: This is only called server side.
*
* @param golem
@@ -6,9 +6,8 @@ import net.minecraft.client.model.ModelRenderer;
import net.minecraft.client.renderer.GLAllocation;
import net.minecraft.client.renderer.Tessellator;
import net.minecraft.entity.Entity;
import net.minecraft.util.ResourceLocation;
import org.lwjgl.opengl.GL11;
import thaumicenergistics.common.ThaumicEnergistics;
import thaumicenergistics.common.items.ItemGolemWirelessBackpack.BackpackSkins;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
@@ -16,8 +15,6 @@ import cpw.mods.fml.relauncher.SideOnly;
public class ModelGolemWifiBackpack
extends ModelBase
{
public static ResourceLocation TEXTURE = new ResourceLocation( ThaumicEnergistics.MOD_ID, "textures/models/golem.wifi.backpack.png" );
/**
* Submodels
*/
@@ -182,12 +179,12 @@ public class ModelGolemWifiBackpack
public void render( final Entity entity, final float f, final float f1, final float f2, final float f3, final float f4,
final float f5 )
{
this.render( 0.03f, f5, true );
this.render( 0.03f, f5, true, BackpackSkins.Thaumium );
}
public void render( final float pearlRotation, final float f5, final boolean inRange )
public void render( final float pearlRotation, final float f5, final boolean inRange, final BackpackSkins skin )
{
Minecraft.getMinecraft().renderEngine.bindTexture( ModelGolemWifiBackpack.TEXTURE );
Minecraft.getMinecraft().renderEngine.bindTexture( skin.getTextureLocation() );
this.PackBack.render( f5 );
this.PackFront.render( f5 );
this.Antenna.render( f5 );
@@ -86,6 +86,11 @@ public class ContainerDistillationPatternEncoder
*/
private final TheInternalInventory internalInventory;
/**
* Cached versions of the source and pattern.
*/
private ItemStack cachedSource, cachedPattern;
/**
* Slot holding the source item.
*/
@@ -179,20 +184,6 @@ public class ContainerDistillationPatternEncoder
}
}
/**
* Returns true if the slot needs to be sync'd
*
* @param slot
* @return
*/
private boolean doesSlotNeedSync( final Slot slot )
{
ItemStack slotStack = slot.getStack();
ItemStack invStack = (ItemStack)this.inventoryItemStacks.get( slot.slotNumber );
return !ItemStack.areItemStacksEqual( invStack, slotStack );
}
/**
* Returns true if the transfer was handled.
* Assumes that clickedslot has an itemstack.
@@ -249,7 +240,8 @@ public class ContainerDistillationPatternEncoder
private void loadPattern()
{
// Read the pattern
this.patternHelper.readPattern( this.slotPatternEncoded.getStack() );
this.cachedPattern = this.slotPatternEncoded.getStack();
this.patternHelper.readPattern( this.cachedPattern );
// Is the pattern valid?
if( this.patternHelper.isValid() )
@@ -270,27 +262,21 @@ public class ContainerDistillationPatternEncoder
*/
private void scanSourceItem( final boolean setSelectedAspect )
{
if( EffectiveSide.isClientSide() )
{
// Ignored client side
return;
}
this.cachedSource = this.slotSourceItem.getStack();
// Clear aspect slots
this.clearAspectSlots();
// Null?
if( !this.slotSourceItem.getHasStack() )
if( this.cachedSource == null )
{
// Done
return;
}
// Get the itemstack
ItemStack sourceItem = this.slotSourceItem.getDisplayStack();
// Get the aspects
AspectList itemAspects = ThaumcraftApiHelper.getObjectAspects( sourceItem );
AspectList itemAspects = ThaumcraftApiHelper.getObjectAspects( this.cachedSource );
Aspect[] sortedAspects = null;
// Does the item have any aspects?
@@ -301,7 +287,7 @@ public class ContainerDistillationPatternEncoder
}
// Generate hash
int hash = ScanManager.generateItemHash( sourceItem.getItem(), sourceItem.getItemDamage() );
int hash = ScanManager.generateItemHash( this.cachedSource.getItem(), this.cachedSource.getItemDamage() );
// Get the list of scanned objects
List<String> list = Thaumcraft.proxy.getScannedObjects().get( this.player.getCommandSenderName() );
@@ -355,14 +341,14 @@ public class ContainerDistillationPatternEncoder
protected boolean detectAndSendChangesMP( final EntityPlayerMP playerMP )
{
// Does the pattern slot need to be sync'd?
if( this.doesSlotNeedSync( this.slotPatternEncoded ) )
if( this.slotPatternEncoded.getStack() != this.cachedPattern )
{
// Load the pattern
this.loadPattern();
return true;
}
// Does the source item need to be sync'd?
else if( this.doesSlotNeedSync( this.slotSourceItem ) )
else if( this.slotSourceItem.getStack() != this.cachedSource )
{
// Scan the source item
this.scanSourceItem( true );
@@ -513,7 +499,16 @@ public class ContainerDistillationPatternEncoder
ItemStack heldItem = player.inventory.getItemStack();
// Set the source slot
this.slotSourceItem.putStack( heldItem );
if( heldItem != null )
{
ItemStack copy = heldItem.copy();
copy.stackSize = 1;
this.slotSourceItem.putStack( copy );
}
else
{
this.slotSourceItem.putStack( null );
}
handled = true;
}
@@ -247,10 +247,13 @@ public abstract class AIAENetworkGolem
return 0;
}
// Calculate the amount to inject
int amountToInject = Math.min( amount, this.maxEssentiaRate );
// Attempt to inject
long amountRejected = monitor.injectEssentia( aspect, amountToInject, Actionable.MODULATE, this.actionSource, true );
// Return the amount that was injected
return amountToInject - amountRejected;
}
@@ -1,10 +1,13 @@
package thaumicenergistics.common.entities;
import net.minecraft.block.Block;
import net.minecraft.client.Minecraft;
import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.init.Blocks;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import org.lwjgl.opengl.GL11;
import thaumcraft.common.config.ConfigBlocks;
import thaumcraft.common.entities.golems.EntityGolemBase;
import thaumicenergistics.api.entities.IGolemHookHandler;
import thaumicenergistics.api.entities.IGolemHookSyncRegistry;
@@ -12,28 +15,42 @@ import thaumicenergistics.client.render.model.ModelGolemWifiBackpack;
import thaumicenergistics.common.integration.tc.GolemCoreType;
import thaumicenergistics.common.items.ItemEnum;
import thaumicenergistics.common.items.ItemGolemWirelessBackpack;
import thaumicenergistics.common.items.ItemGolemWirelessBackpack.BackpackSkins;
import thaumicenergistics.common.utils.EffectiveSide;
import appeng.api.AEApi;
import appeng.items.parts.ItemFacade;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
public class WirelessGolemHandler
implements IGolemHookHandler
{
/**
* Data stored on the client side.
*
* @author Nividica
*
*/
public static class WirelessClientData
{
public boolean isInRange = false;
public float pearlRotation = 0.0f;
public WirelessClientData( final boolean inRange )
{
this.isInRange = inRange;
}
public BackpackSkins skin = BackpackSkins.Thaumium;
}
/**
* Data stored on the server side.
*
* @author Nividica
*
*/
public static class WirelessServerData
{
public final String encryptionKey;
public int tickCounter = 0;
public boolean isInRange = false;
public ItemStack facade = null;
public BackpackSkins skin = BackpackSkins.Thaumium;
public WirelessServerData( final String encKey )
{
@@ -44,16 +61,17 @@ public class WirelessGolemHandler
private static WirelessGolemHandler INSTANCE = null;
/**
* NBT key used to represent if the golem has the wifi backpack or not.
* NBT keys
*/
private static final String NBTKEY_WIFI_KEY = "wifiBackpackKey";
private static final String NBTKEY_WIFI_KEY = "wifiBackpackKey", NBTKEY_FACADE = "facade";
/**
* State flags.
*/
private static final Character SYNCFLAG_HAS_WIFI_IN_RANGE = Character.valueOf( 'i' ),
SYNCFLAG_HAS_WIFI_OUT_OF_RANGE = Character.valueOf( 'o' ),
SYNCFLAG_NO_WIFI = Character.valueOf( 'n' );
SYNCFLAG_NO_WIFI = Character.valueOf( 'n' ),
SYNCFIELD_SKIN = 'a';
@SideOnly(Side.CLIENT)
private ModelGolemWifiBackpack addonModel;
@@ -63,11 +81,21 @@ public class WirelessGolemHandler
*/
private int wifiSyncID = -1;
/**
* ID of the skin sync field.
*/
private int skinSyncID = -1;
/**
* Cache of the backpack item.
*/
private ItemGolemWirelessBackpack backpackItem;
/**
* Item that represents a facade, or null.
*/
private ItemFacade facadeItem;
/**
* Private constructor.
*/
@@ -81,6 +109,9 @@ public class WirelessGolemHandler
// Store a reference to the backpack item.
this.backpackItem = (ItemGolemWirelessBackpack)ItemEnum.GOLEM_WIFI_BACKPACK.getItem();
// Store a reference to the facade item
this.facadeItem = (ItemFacade)AEApi.instance().definitions().items().facade().maybeItem().orNull();
}
/**
@@ -98,61 +129,45 @@ public class WirelessGolemHandler
}
/**
* Creates the wifi addon model.
* Gets a character based on the skin.
*
* @param skin
* @return
*/
@SideOnly(Side.CLIENT)
private void setupModel()
private Character backpackSkinToSyncChar( final BackpackSkins skin )
{
this.addonModel = new ModelGolemWifiBackpack();
}
@Override
public void addDefaultSyncEntries( final IGolemHookSyncRegistry syncRegistry )
{
// Register the wifi entry
this.wifiSyncID = syncRegistry.registerSyncChar( this, SYNCFLAG_NO_WIFI );
}
@Override
public void bellLeftClicked( final EntityGolemBase golem, final Object handlerData, final ItemStack itemGolemPlacer, final EntityPlayer player,
final boolean dismantled, final Side side )
{
// Goes the golem have a backpack on?
if( handlerData instanceof WirelessServerData )
int id = 0;
if( skin != null )
{
// Create a new backpack item
ItemStack backpack = ItemEnum.GOLEM_WIFI_BACKPACK.getStack();
// Set the key
this.backpackItem.setEncryptionKey( backpack, ( (WirelessServerData)handlerData ).encryptionKey, null );
// Drop the backpack
golem.entityDropItem( backpack, 0.5f );
id = skin.ordinal();
}
return Character.valueOf( (char)( SYNCFIELD_SKIN + id ) );
}
@Override
public boolean canHandleInteraction( final EntityGolemBase golem, final Object handlerData, final EntityPlayer player, final Side side )
/**
* Han the backpack be handled?
*
* @param golem
* @param handlerData
* @param player
* @param side
* @return
*/
private InteractionLevel canHandleInteration_Backpack( final EntityGolemBase golem, final Object handlerData, final EntityPlayer player,
final Side side )
{
// Get the held item
ItemStack heldItem = player.inventory.getCurrentItem();
// Check for backpack
boolean isHoldingBackpack = ( ( heldItem != null ) && ( heldItem.getItem() == this.backpackItem ) );
// Does the golem already have a backpack on?
if( handlerData != null )
{
// Already has backpack.
return false;
return InteractionLevel.NoInteraction;
}
// Is the core valid?
GolemCoreType core = GolemCoreType.getCoreByID( golem.getCore() );
if( core == null )
{
return false;
return InteractionLevel.NoInteraction;
}
switch ( core )
{
@@ -166,7 +181,7 @@ public class WirelessGolemHandler
case Sorting:
case Use:
default:
return false;
return InteractionLevel.NoInteraction;
// Supported cores
case Gather:
@@ -179,36 +194,274 @@ public class WirelessGolemHandler
// Client side?
if( side == Side.CLIENT )
{
if( isHoldingBackpack )
{
//Swing the item
player.swingItem();
}
return false;
//Swing the item
player.swingItem();
return InteractionLevel.NoInteraction;
}
return isHoldingBackpack;
return InteractionLevel.FullInteraction;
}
/**
* Can the facade be handled?
*
* @param golem
* @param handlerData
* @param player
* @param side
* @param heldItem
* @return
*/
private InteractionLevel canHandleInteration_Facade( final EntityGolemBase golem, final Object handlerData, final EntityPlayer player,
final Side side,
final ItemStack heldItem )
{
// Is the golem wearing a backpack?
if( handlerData == null )
{
// No backpack to apply it to
return InteractionLevel.NoInteraction;
}
// Get the skin for the facade
BackpackSkins skin = this.getSkinFromFacade( heldItem );
if( skin == null )
{
// Unsupported facade
return InteractionLevel.NoInteraction;
}
// Client side?
if( ( side == Side.CLIENT ) && ( handlerData instanceof WirelessClientData ) )
{
//Swing the item
player.swingItem();
// Set the skin
( (WirelessClientData)handlerData ).skin = skin;
return InteractionLevel.NoInteraction;
}
return InteractionLevel.SyncInteraction;
}
/**
* Returns the skin associated with the facade, or null if there is not one.
*
* @param facade
* @return
*/
private BackpackSkins getSkinFromFacade( final ItemStack facade )
{
Block b = this.facadeItem.getBlock( facade );
int metaData = this.facadeItem.getMeta( facade );
if( b == ConfigBlocks.blockCosmeticSolid )
{
// Thaumium block?
if( metaData == 4 )
{
return BackpackSkins.Thaumium;
}
// Tallow block?
else if( metaData == 5 )
{
return BackpackSkins.Tallow;
}
}
// Greatwood log?
else if( ( b == ConfigBlocks.blockMagicalLog ) && ( metaData == 0 ) )
{
return BackpackSkins.GreatWood;
}
// Flesh?
else if( ( b == ConfigBlocks.blockTaint ) && ( metaData == 2 ) )
{
return BackpackSkins.Flesh;
}
// Stone block?
else if( b == Blocks.stone )
{
return BackpackSkins.Stone;
}
// Hay block?
else if( b == Blocks.hay_block )
{
return BackpackSkins.Straw;
}
// Bricks?
else if( b == Blocks.brick_block )
{
return BackpackSkins.Clay;
}
// Iron?
else if( b == Blocks.iron_block )
{
return BackpackSkins.Iron;
}
else if( b == Blocks.gold_block )
{
return BackpackSkins.Gold;
}
else if( b == Blocks.diamond_block )
{
return BackpackSkins.Diamond;
}
return null;
}
/**
* Creates the wifi addon model.
*/
@SideOnly(Side.CLIENT)
private void setupModel()
{
this.addonModel = new ModelGolemWifiBackpack();
}
/**
* Gets the skin based on the character
*
* @param skinSyncChar
* @return
*/
private BackpackSkins syncCharToBackpackSkin( final char skinSyncChar )
{
int id = skinSyncChar - SYNCFIELD_SKIN;
if( ( id < 0 ) || ( id > BackpackSkins.VALUES.length ) )
{
id = 0;
}
return BackpackSkins.VALUES[id];
}
@Override
public void addDefaultSyncEntries( final IGolemHookSyncRegistry syncRegistry )
{
// Register the wifi entry
this.wifiSyncID = syncRegistry.registerSyncChar( this, SYNCFLAG_NO_WIFI );
// Register facade item
this.skinSyncID = syncRegistry.registerSyncChar( this, this.backpackSkinToSyncChar( BackpackSkins.Thaumium ) );
}
@Override
public void bellLeftClicked( final EntityGolemBase golem, final Object handlerData, final ItemStack itemGolemPlacer, final EntityPlayer player,
final boolean dismantled, final Side side )
{
// Goes the golem have a backpack on?
if( handlerData instanceof WirelessServerData )
{
WirelessServerData wsd = ( (WirelessServerData)handlerData );
// Create a new backpack item
ItemStack backpack = ItemEnum.GOLEM_WIFI_BACKPACK.getStack();
// Set the key
this.backpackItem.setEncryptionKey( backpack, wsd.encryptionKey, null );
// Drop the backpack
golem.entityDropItem( backpack, 0.5f );
// Is there a facade?
if( wsd.facade != null )
{
// Drop the facade
golem.entityDropItem( wsd.facade, 0.5f );
}
}
}
@Override
public Object customInteraction( final EntityGolemBase golem, final Object handlerData, final EntityPlayer player, final Side side )
public InteractionLevel canHandleInteraction( final EntityGolemBase golem, final Object handlerData, final EntityPlayer player, final Side side )
{
// Get the held item
ItemStack heldItem = player.inventory.getCurrentItem();
// Is the backpack linked?
String encKey = this.backpackItem.getEncryptionKey( heldItem );
if( encKey != null )
// Empty hand?
if( heldItem == null )
{
return InteractionLevel.NoInteraction;
}
// Backpack?
if( heldItem.getItem() == this.backpackItem )
{
return this.canHandleInteration_Backpack( golem, handlerData, player, side );
}
// Facade?
else if( ( this.facadeItem != null ) && ( heldItem.getItem() == this.facadeItem ) )
{
return this.canHandleInteration_Facade( golem, handlerData, player, side, heldItem );
}
return InteractionLevel.NoInteraction;
}
@Override
public Object customInteraction( final EntityGolemBase golem, final Object handlerData, final IGolemHookSyncRegistry syncData,
final EntityPlayer player, final Side side )
{
// Get the held item
ItemStack heldItem = player.inventory.getCurrentItem();
// Backpack?
if( heldItem.getItem() == this.backpackItem )
{
// Is the backpack linked?
String encKey = this.backpackItem.getEncryptionKey( heldItem );
if( encKey != null )
{
// Play sound
golem.worldObj.playSoundAtEntity( golem, "thaumcraft:upgrade", 0.5F, 1.0F );
// Take the item
player.inventory.setInventorySlotContents( player.inventory.currentItem, null );
// Create the data
return new WirelessServerData( encKey );
}
}
// Facade?
else if( ( this.facadeItem != null ) && ( heldItem.getItem() == this.facadeItem ) )
{
WirelessServerData wsd = (WirelessServerData)handlerData;
// Is there a facade set?
if( ( wsd.facade != null ) && ( !player.capabilities.isCreativeMode ) )
{
// Drop the old facade
golem.entityDropItem( wsd.facade, 0.5f );
}
// Set facade
wsd.facade = heldItem.copy();
wsd.facade.stackSize = 1;
// Take a facade from the player
if( !player.capabilities.isCreativeMode )
{
--heldItem.stackSize;
if( heldItem.stackSize <= 0 )
{
player.inventory.setInventorySlotContents( player.inventory.currentItem, null );
}
}
// Set the skin
wsd.skin = this.getSkinFromFacade( wsd.facade );
syncData.updateSyncChar( this, this.skinSyncID, this.backpackSkinToSyncChar( wsd.skin ) );
// Play sound
golem.worldObj.playSoundAtEntity( golem, "thaumcraft:upgrade", 0.5F, 1.0F );
golem.worldObj.playSoundAtEntity( golem, "thaumcraft:cameraticks", 0.5F, 1.0F );
// Take the item
player.inventory.setInventorySlotContents( player.inventory.currentItem, null );
// Create the data
return new WirelessServerData( encKey );
return wsd;
}
return null;
@@ -220,9 +473,18 @@ public class WirelessGolemHandler
// Has backpack?
if( serverHandlerData instanceof WirelessServerData )
{
// Update clients
syncData.updateSyncChar( this, this.wifiSyncID, ( ( (WirelessServerData)serverHandlerData ).isInRange ? SYNCFLAG_HAS_WIFI_IN_RANGE
: SYNCFLAG_HAS_WIFI_OUT_OF_RANGE ) );
WirelessServerData wsd = (WirelessServerData)serverHandlerData;
// Set sync data every 18th tick
if( ++wsd.tickCounter > 18 )
{
// Reset counter
wsd.tickCounter = 0;
// Set sync data
syncData.updateSyncChar( this, this.wifiSyncID, ( wsd.isInRange ? SYNCFLAG_HAS_WIFI_IN_RANGE : SYNCFLAG_HAS_WIFI_OUT_OF_RANGE ) );
syncData.updateSyncChar( this, this.skinSyncID, this.backpackSkinToSyncChar( wsd.skin ) );
}
}
}
@@ -241,15 +503,31 @@ public class WirelessGolemHandler
@Override
public Object readEntityFromNBT( final EntityGolemBase golem, final NBTTagCompound nbtTag )
{
WirelessServerData wsd = null;
// Does the golem have a backpack?
if( nbtTag.hasKey( NBTKEY_WIFI_KEY ) )
{
// Create the data
return new WirelessServerData( nbtTag.getString( NBTKEY_WIFI_KEY ) );
wsd = new WirelessServerData( nbtTag.getString( NBTKEY_WIFI_KEY ) );
if( nbtTag.hasKey( NBTKEY_FACADE ) )
{
// Read the facade
try
{
wsd.facade = ItemStack.loadItemStackFromNBT( nbtTag.getCompoundTag( NBTKEY_FACADE ) );
wsd.skin = this.getSkinFromFacade( wsd.facade );
}
catch( Exception e )
{
// Invalid facade
}
}
}
// No wifi
return null;
return wsd;
}
@Override
@@ -297,7 +575,7 @@ public class WirelessGolemHandler
GL11.glPushMatrix();
// Translate to the golem
GL11.glTranslatef( (float)x, (float)y + 0.63f, (float)z );
GL11.glTranslatef( (float)x, (float)y + 0.64f, (float)z );
// Rotate to face away from the golem
float golemYaw = 270.0f;
@@ -315,7 +593,7 @@ public class WirelessGolemHandler
GL11.glScalef( 0.8f, 0.8f, 0.8f );
// Render
this.addonModel.render( wcd.pearlRotation, 0.0625f, golemActive );
this.addonModel.render( wcd.pearlRotation, 0.0625f, golemActive, wcd.skin );
// Pop the matrix
GL11.glPopMatrix();
@@ -405,15 +683,25 @@ public class WirelessGolemHandler
}
// Has existing data?
WirelessClientData wcd = null;
if( clientHandlerData instanceof WirelessClientData )
{
// Update the data
( (WirelessClientData)clientHandlerData ).isInRange = inRange;
return clientHandlerData;
wcd = (WirelessClientData)clientHandlerData;
}
else
{
// Create new data
wcd = new WirelessClientData();
}
// Create new data
return new WirelessClientData( inRange );
// Update the range
wcd.isInRange = inRange;
// Get the skin
char skinChar = syncData.getSyncCharOrDefault( this.skinSyncID, SYNCFIELD_SKIN );
wcd.skin = this.syncCharToBackpackSkin( skinChar );
return wcd;
}
@Override
@@ -422,8 +710,17 @@ public class WirelessGolemHandler
// Does the golem have a backpack?
if( serverHandlerData instanceof WirelessServerData )
{
WirelessServerData wsd = ( (WirelessServerData)serverHandlerData );
// Write network key
nbtTag.setString( NBTKEY_WIFI_KEY, ( (WirelessServerData)serverHandlerData ).encryptionKey );
nbtTag.setString( NBTKEY_WIFI_KEY, wsd.encryptionKey );
// Has facade?
if( wsd.facade != null )
{
// Write the facade
nbtTag.setTag( NBTKEY_FACADE, wsd.facade.writeToNBT( new NBTTagCompound() ) );
}
}
}
@@ -92,6 +92,7 @@ public class FeatureGolemBackpack
new ResearchPage( ResearchTypes.GOLEM_BACKPACK.getPageName( 1 ) ),
new ResearchPage( RecipeRegistry.ITEM_GOLEM_WIFI_BACKPACK ),
new ResearchPage( ResearchTypes.GOLEM_BACKPACK.getPageName( 2 ) ),
new ResearchPage( ResearchTypes.GOLEM_BACKPACK.getPageName( 3 ) ),
new ResearchPage( ResearchTypes.GOLEM_BACKPACK.getPageName( "COREGATHER" ) ),
new ResearchPage( "COREFILL", ResearchTypes.GOLEM_BACKPACK.getPageName( "COREFILL" ) ),
new ResearchPage( "CORELIQUID", ResearchTypes.GOLEM_BACKPACK.getPageName( "CORELIQUID" ) ),
@@ -103,7 +104,8 @@ public class FeatureGolemBackpack
ResearchTypes.GOLEM_BACKPACK.createResearchItem( aspects, ResearchRegistry.COMPLEXITY_LARGE, icon, pages );
// Set parents
ResearchTypes.GOLEM_BACKPACK.researchItem.setParents( this.getFirstValidParentKey( false ), PseudoResearchTypes.COREGATHER.getKey() );
ResearchTypes.GOLEM_BACKPACK.researchItem.setParents( this.getFirstValidParentKey( false ), "COREGATHER",
PseudoResearchTypes.COREGATHER.getKey() );
// Hide until the parent has been researched
ResearchTypes.GOLEM_BACKPACK.researchItem.setConcealed();
@@ -514,14 +514,21 @@ public class EssentiaMonitor
}
// Simulate the injection
long rejectedAmount = this.injectEssentiaSafely( aspect, amount, Actionable.SIMULATE, source, essentiaGas );
long adjustedAmount = amount - rejectedAmount;
long injectedAmount = amount;
long rejectedAmount = this.injectEssentiaSafely( aspect, injectedAmount, Actionable.SIMULATE, source, essentiaGas );
injectedAmount -= rejectedAmount;
// Was all rejected?
if( injectedAmount == 0 )
{
return amount;
}
// Is this a powered injection?
if( powered )
{
// Simulate power extraction
double powerRequest = EssentiaMonitor.AE_PER_ESSENTIA * adjustedAmount;
double powerRequest = EssentiaMonitor.AE_PER_ESSENTIA * injectedAmount;
double powerReceived = this.energyGrid.extractAEPower( powerRequest, Actionable.SIMULATE, PowerMultiplier.CONFIG );
// Was enough power extracted?
@@ -536,18 +543,18 @@ public class EssentiaMonitor
if( mode == Actionable.MODULATE )
{
// Inject
rejectedAmount = this.injectEssentiaSafely( aspect, adjustedAmount, Actionable.MODULATE, source, essentiaGas );
adjustedAmount -= rejectedAmount;
rejectedAmount = this.injectEssentiaSafely( aspect, injectedAmount, Actionable.MODULATE, source, essentiaGas );
injectedAmount -= rejectedAmount;
// Adjust and extract power
if( powered )
{
double powerRequest = EssentiaMonitor.AE_PER_ESSENTIA * adjustedAmount;
double powerRequest = EssentiaMonitor.AE_PER_ESSENTIA * injectedAmount;
this.energyGrid.extractAEPower( powerRequest, Actionable.MODULATE, PowerMultiplier.CONFIG );
}
}
return rejectedAmount;
return amount - injectedAmount;
}
@Override
@@ -24,6 +24,86 @@ import codechicken.nei.recipe.IRecipeHandler;
public class ModuleNEI
{
/**
* Base class for handling item overlays.
*
* @author Nividica
*
*/
abstract class AbstractBaseOverlayHandler
implements IOverlayHandler
{
/**
* Calls on the subclass to add an ingredient to the items array.
*
* @param ingredient
* @param overlayItems
*/
protected abstract boolean addIngredientToItems( PositionedStack ingredient, IAEItemStack[] overlayItems );
/**
* Called when the items are ready to be placed in the GUI.
*
* @param overlayItems
*/
protected abstract void addItemsToGUI( IAEItemStack[] overlayItems );
/**
* Checks with the subclass to see if this is a GUI it handles.
*
* @param gui
* @return
*/
protected abstract boolean isCorrectGUI( GuiContainer gui );
/**
* Called when the user has shift-clicked the [?] button in NEI
*/
@Override
public final void overlayRecipe( final GuiContainer gui, final IRecipeHandler recipeHandler, final int recipeIndex, final boolean shift )
{
try
{
// Ensure the gui is correct
if( this.isCorrectGUI( gui ) )
{
// List of items
IAEItemStack[] overlayItems = new IAEItemStack[9];
// Assume there are no items until they are added.
boolean hasItems = false;
// Get the ingredients
List<PositionedStack> ingredients = recipeHandler.getIngredientStacks( recipeIndex );
// Get each item
for( PositionedStack ingredient : ingredients )
{
// Skip nulls
if( ( ingredient == null ) || ( ingredient.item == null ) || ( ingredient.item.getItem() == null ) )
{
continue;
}
// Pass to subclass
hasItems |= this.addIngredientToItems( ingredient, overlayItems );
}
// Were any items added?
if( hasItems )
{
this.addItemsToGUI( overlayItems );
}
}
}
catch( Exception e )
{
// Silently ignored.
}
}
}
/**
* Sends a selected NEI recipe to the open A.C.T on the server.
*
@@ -178,86 +258,6 @@ public class ModuleNEI
}
/**
* Base class for handling item overlays.
*
* @author Nividica
*
*/
abstract class AbstractBaseOverlayHandler
implements IOverlayHandler
{
/**
* Calls on the subclass to add an ingredient to the items array.
*
* @param ingredient
* @param overlayItems
*/
protected abstract boolean addIngredientToItems( PositionedStack ingredient, IAEItemStack[] overlayItems );
/**
* Called when the items are ready to be placed in the GUI.
*
* @param overlayItems
*/
protected abstract void addItemsToGUI( IAEItemStack[] overlayItems );
/**
* Checks with the subclass to see if this is a GUI it handles.
*
* @param gui
* @return
*/
protected abstract boolean isCorrectGUI( GuiContainer gui );
/**
* Called when the user has shift-clicked the [?] button in NEI
*/
@Override
public final void overlayRecipe( final GuiContainer gui, final IRecipeHandler recipeHandler, final int recipeIndex, final boolean shift )
{
try
{
// Ensure the gui is correct
if( this.isCorrectGUI( gui ) )
{
// List of items
IAEItemStack[] overlayItems = new IAEItemStack[9];
// Assume there are no items until they are added.
boolean hasItems = false;
// Get the ingredients
List<PositionedStack> ingredients = recipeHandler.getIngredientStacks( recipeIndex );
// Get each item
for( PositionedStack ingredient : ingredients )
{
// Skip nulls
if( ( ingredient == null ) || ( ingredient.item == null ) || ( ingredient.item.getItem() == null ) )
{
continue;
}
// Pass to subclass
hasItems |= this.addIngredientToItems( ingredient, overlayItems );
}
// Were any items added?
if( hasItems )
{
this.addItemsToGUI( overlayItems );
}
}
}
catch( Exception e )
{
// Silently ignored.
}
}
}
/**
* Starting X offset for slots in NEI
*/
@@ -8,6 +8,7 @@ import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import thaumcraft.common.entities.golems.EntityGolemBase;
import thaumicenergistics.api.entities.IGolemHookHandler;
import thaumicenergistics.api.entities.IGolemHookHandler.InteractionLevel;
import thaumicenergistics.api.entities.IGolemHookSyncRegistry;
import thaumicenergistics.common.utils.EffectiveSide;
import thaumicenergistics.common.utils.ThELog;
@@ -15,6 +16,13 @@ import thaumicenergistics.fml.ThECore;
import cpw.mods.fml.relauncher.Side;
import cpw.mods.fml.relauncher.SideOnly;
/**
* Golem hook system.<br>
* Manages golem hooks and handlers.
*
* @author Nividica
*
*/
public class GolemHooks
{
private static class DummyHookHandler
@@ -40,13 +48,15 @@ public class GolemHooks
}
@Override
public boolean canHandleInteraction( final EntityGolemBase golem, final Object handlerData, final EntityPlayer player, final Side side )
public InteractionLevel canHandleInteraction( final EntityGolemBase golem, final Object handlerData, final EntityPlayer player,
final Side side )
{
return false;
return InteractionLevel.NoInteraction;
}
@Override
public Object customInteraction( final EntityGolemBase golem, final Object handlerData, final EntityPlayer player, final Side side )
public Object customInteraction( final EntityGolemBase golem, final Object handlerData, final IGolemHookSyncRegistry syncData,
final EntityPlayer player, final Side side )
{
return null;
}
@@ -216,56 +226,91 @@ public class GolemHooks
public static boolean hook_CustomInteraction( final EntityGolemBase golem, final EntityPlayer player,
final HashMap<IGolemHookHandler, Object> golemHandlerData )
{
boolean interactionHandled = false;
GolemSyncRegistry syncRegistry = ( (GolemSyncRegistry)golemHandlerData.get( internalHandler ) );
boolean needsSync = false;
boolean needsSetup = false;
boolean skipGUI = false;
InteractionLevel handlerLevel;
Side side = EffectiveSide.side();
for( IGolemHookHandler handler : registeredHandlers )
{
// Get the current handler data
Object handlerData = golemHandlerData.getOrDefault( handler, null );
// Call handler
try
{
// Get the current handler data
Object handlerData = golemHandlerData.getOrDefault( handler, null );
boolean hadData = ( handlerData != null );
// Call handler
interactionHandled |= handler.canHandleInteraction( golem, handlerData, player, side );
if( interactionHandled )
{
try
{
handlerData = handler.customInteraction( golem, handlerData, player, side );
// Update golem
if( handlerData == null )
{
if( hadData )
{
golemHandlerData.remove( handler );
}
}
else
{
golemHandlerData.put( handler, handlerData );
}
}
catch( Exception e )
{
logCaughtException( "customInteraction", handler, e );
}
}
handlerLevel = handler.canHandleInteraction( golem, handlerData, player, side );
}
catch( Exception e )
{
logCaughtException( "canHandleInteraction", handler, e );
continue;
}
switch ( handlerLevel )
{
case NoInteraction:
continue;
case BasicInteraction:
break;
case SyncInteraction:
needsSync = true;
break;
case FullInteraction:
needsSync = true;
needsSetup = true;
break;
}
// Handled, dont show the GUI
skipGUI = true;
boolean hadData = ( handlerData != null );
try
{
handlerData = handler.customInteraction( golem, handlerData, syncRegistry, player, side );
}
catch( Exception e )
{
logCaughtException( "customInteraction", handler, e );
continue;
}
// Update golem
if( handlerData == null )
{
if( hadData )
{
golemHandlerData.remove( handler );
}
}
else
{
golemHandlerData.put( handler, handlerData );
}
}
if( interactionHandled && side == Side.SERVER )
if( side == Side.SERVER )
{
golem.setupGolem();
if( needsSetup )
{
golem.setupGolem();
}
if( needsSync )
{
syncRegistry.markDirty();
}
}
return interactionHandled;
return skipGUI;
}
/**
@@ -314,7 +359,14 @@ public class GolemHooks
else if( DATAWATCHER_ID > 0 )
{
// Add datawatcher field.
watcher.addObject( DATAWATCHER_ID, localRegistry.mappingsToString() );
try
{
watcher.addObject( DATAWATCHER_ID, localRegistry.mappingsToString() );
}
catch( Exception e )
{
localRegistry.markDirty();
}
}
}
@@ -335,7 +387,14 @@ public class GolemHooks
// Update handlers
for( IGolemHookHandler handler : dynamicHandlers )
{
handler.golemTick( golem, golemHandlerData.get( handler ), syncRegistry );
try
{
handler.golemTick( golem, golemHandlerData.get( handler ), syncRegistry );
}
catch( Exception e )
{
logCaughtException( "golemTick", handler, e );
}
}
// Update data watcher
@@ -343,7 +402,22 @@ public class GolemHooks
{
if( DATAWATCHER_ID > 0 )
{
golem.getDataWatcher().updateObject( DATAWATCHER_ID, syncRegistry.mappingsToString() );
try
{
golem.getDataWatcher().updateObject( DATAWATCHER_ID, syncRegistry.mappingsToString() );
}
catch( NullPointerException e1 )
{
try
{
golem.getDataWatcher().addObject( DATAWATCHER_ID, syncRegistry.mappingsToString() );
}
catch( Exception e2 )
{
syncRegistry.markDirty();
}
}
}
}
return;
@@ -364,10 +438,18 @@ public class GolemHooks
syncRegistry.clientSyncTicks = 0.0f;
// Is the data out of sync?
String watcherString = golem.getDataWatcher().getWatchableObjectString( DATAWATCHER_ID );
if( !syncRegistry.hasChanged() && ( watcherString == syncRegistry.lastUpdatedFrom ) )
String watcherString;
try
{
watcherString = golem.getDataWatcher().getWatchableObjectString( DATAWATCHER_ID );
if( !syncRegistry.hasChanged() && ( watcherString == syncRegistry.lastUpdatedFrom ) )
{
// Data is in sync
return;
}
}
catch( Exception e )
{
// Data is in sync
return;
}
@@ -382,31 +464,31 @@ public class GolemHooks
// Inform each handler
for( IGolemHookHandler handler : handlersToUpdate )
{
// Get the current handler data
Object handlerData = golemHandlerData.getOrDefault( handler, null );
boolean hadData = ( handlerData != null );
try
{
// Get the current handler data
Object handlerData = golemHandlerData.getOrDefault( handler, null );
boolean hadData = ( handlerData != null );
// Call handler
handlerData = handler.syncDataChanged( syncRegistry, handlerData );
// Update golem
if( handlerData == null )
{
if( hadData )
{
golemHandlerData.remove( handler );
}
}
else
{
golemHandlerData.put( handler, handlerData );
}
}
catch( Exception e )
{
logCaughtException( "onSyncDataChanged", handler, e );
continue;
}
// Update golem
if( handlerData == null )
{
if( hadData )
{
golemHandlerData.remove( handler );
}
}
else
{
golemHandlerData.put( handler, handlerData );
}
}
}
@@ -562,9 +644,16 @@ public class GolemHooks
}
// Update data watcher
if( localRegistry.hasChanged() && side == Side.SERVER )
if( localRegistry.hasChanged() && ( side == Side.SERVER ) && ( DATAWATCHER_ID > 0 ) )
{
golem.getDataWatcher().updateObject( DATAWATCHER_ID, localRegistry.mappingsToString() );
try
{
golem.getDataWatcher().updateObject( DATAWATCHER_ID, localRegistry.mappingsToString() );
}
catch( NullPointerException e )
{
localRegistry.markDirty();
}
}
}
@@ -120,6 +120,14 @@ class GolemSyncRegistry
return "";
}
/**
* Marks the registry as needing to be syncd.
*/
public void markDirty()
{
this.hasDataChanged = true;
}
/**
* Reads the mappings from a string
*
@@ -5,6 +5,7 @@ import net.minecraft.entity.player.EntityPlayer;
import net.minecraft.item.Item;
import net.minecraft.item.ItemStack;
import net.minecraft.nbt.NBTTagCompound;
import net.minecraft.util.ResourceLocation;
import thaumicenergistics.common.ThaumicEnergistics;
import thaumicenergistics.common.registries.ThEStrings;
import appeng.api.features.INetworkEncodable;
@@ -16,6 +17,59 @@ public class ItemGolemWirelessBackpack
extends Item
implements INetworkEncodable
{
public static enum BackpackSkins
{
Thaumium ("Thaum"),
Stone ("Stone"),
Straw ("Straw"),
GreatWood ("Wood"),
Flesh ("Flesh"),
Clay ("Clay"),
Iron ("Iron"),
Tallow ("Tallow"),
Gold ("Gold"),
Diamond ("Diamond");
/**
* Array of skins.
*/
public static final BackpackSkins VALUES[] = BackpackSkins.values();
/**
* Skin texture.
*/
private ResourceLocation texture = null;
/**
* Texture ID of the skin
*/
private final String texID;
/**
* Private constructor
*
* @param ID
*/
private BackpackSkins( final String ID )
{
this.texID = ID;
}
/**
* Gets the texture for this skin.
*
* @return
*/
public ResourceLocation getTextureLocation()
{
if( this.texture == null )
{
this.texture = new ResourceLocation( ThaumicEnergistics.MOD_ID, "textures/models/golemBackpack/" + this.texID + ".png" );
}
return this.texture;
}
}
private static final String NBT_ENCRYPTION_KEY = "wifiKey";
public ItemGolemWirelessBackpack()
@@ -25,6 +79,8 @@ public class ItemGolemWirelessBackpack
// Set non stacking
this.setMaxStackSize( 1 );
// Set non-damageable
this.setMaxDamage( 0 );
}
@@ -15,6 +15,16 @@ import cpw.mods.fml.relauncher.SideOnly;
public abstract class ThEClientPacket
extends ThEBasePacket
{
@SideOnly(Side.CLIENT)
private final void preWrap()
{
// Set the player
this.player = Minecraft.getMinecraft().thePlayer;
// Execute the packet
this.wrappedExecute();
}
@Override
protected final boolean includePlayerInStream()
{
@@ -30,11 +40,7 @@ public abstract class ThEClientPacket
// Ensure this is client side
if( EffectiveSide.isClientSide() )
{
// Get the player
this.player = Minecraft.getMinecraft().thePlayer;
// Execute the packet
this.wrappedExecute();
this.preWrap();
}
}
}
@@ -163,7 +163,8 @@ public class DistillationPatternHelper
*/
public void setInputItem( final ItemStack inputItem )
{
this.input = inputItem;
this.input = inputItem.copy();
this.input.stackSize = 1;
}
/**
@@ -182,7 +182,8 @@ thaumicenergistics.research_page.TEDISTILLATIONPATTERNENCODER.1=Expanding on the
tc.research_name.thaumicenergistics.TEGOLEMWIFIBACKPACK=Golem Wifi Backpack
tc.research_text.thaumicenergistics.TEGOLEMWIFIBACKPACK=DigitalGolem423@ae2.net
thaumicenergistics.research_page.TEGOLEMWIFIBACKPACK.1=After watching your golems running around depositing gathered items into a chest for you to collect and store later, you wondered if you could cut out the middle-man. Inspiration struck when you realized that you could utilize your networks wireless capabilities to do just that.<BR>After spending hours feverishly working, you created a backpack that will give your golems access to the network. The backpack does come with some limitations however. Golems must be in range of an §lAccess Point§r to communicate with the network. The fluix pearl atop the antenna serves as an indicator to whether or not the golem can communicate with the network.
thaumicenergistics.research_page.TEGOLEMWIFIBACKPACK.2=As communicating with the network takes power, you retro-fitted a charger to draw power from two embedded Nitor. When charged the backpack will be able to transmit for only a short time before starting another charge cycle. During this window of opportunity the golem will be able to interact with the network.<LINE>As you progress with your golemancy research, check back here for potential new uses of the backpack.
thaumicenergistics.research_page.TEGOLEMWIFIBACKPACK.2=As communicating with the network takes power, you retro-fitted a charger to draw power from two embedded Nitor. When charged the backpack will be able to transmit for only a short time before starting another charge cycle. During this window of opportunity the golem will be able to interact with the network.<BR>To link the backpack with your network, place it in the security terminal.<LINE>As you progress with your golemancy research, check back here for potential new uses of the backpack.
thaumicenergistics.research_page.TEGOLEMWIFIBACKPACK.3=§nFacades§r<BR>Facades can be used to apply a variety of styles to the backpack: Thaumium Block(default), Greatwood Log, Bricks, Hay Bale, Tallow Block, Flesh Block, Stone, Iron Block, Gold Block and Diamond Block.<BR>Right click on a golem wearing a backpack with the facade in hand to apply it.
thaumicenergistics.research_page.TEGOLEMWIFIBACKPACK.COREGATHER=§nGather Core Unlocked§r<IMG>thaumicenergistics:textures/gui/states.png:16:0:16:16:1.0</IMG>A golem equipped with a §lGather Core§r will be able to deposit gathered items directly into the network. The maximum number of items deposited is determined by how efficiently the golem manages each charge.
thaumicenergistics.research_page.TEGOLEMWIFIBACKPACK.COREFILL=§nFill Core Unlocked§r<IMG>thaumicenergistics:textures/gui/states.png:32:0:16:16:1.0</IMG>A golem equipped with a §lFill Core§r will be able to extract items from the network. The maximum number of items extracted is determined by the golems GUI, and how efficiently the golem manages each charge.
thaumicenergistics.research_page.TEGOLEMWIFIBACKPACK.CORELIQUID=§nDecanting Core Unlocked§r<IMG>thaumicenergistics:textures/gui/states.png:48:0:16:16:1.0</IMG>A golem equipped with a Decanting Core§r will be able to extract fluid from the network. The maximum amount of fluid extracted is determined by how efficiently the golem manages each charge.<LINE>Special note: Storing fluids in the AE network requires the services of another mod, such as Extra Cells 2.
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB