Calculate terminal displayed items on the server

Avoids sending giant S2C packets containing all items in the network
when the network contains a great variety of different items
This commit is contained in:
Shadowfacts 2021-02-21 12:00:32 -05:00
parent 3ebafc062f
commit f9befe9549
Signed by: shadowfacts
GPG Key ID: 94A5AB95422746E5
9 changed files with 223 additions and 64 deletions

View File

@ -7,6 +7,7 @@ import net.shadowfacts.phycon.init.PhyBlocks
import net.shadowfacts.phycon.init.PhyItems import net.shadowfacts.phycon.init.PhyItems
import net.shadowfacts.phycon.init.PhyScreens import net.shadowfacts.phycon.init.PhyScreens
import net.shadowfacts.phycon.networking.C2STerminalRequestItem import net.shadowfacts.phycon.networking.C2STerminalRequestItem
import net.shadowfacts.phycon.networking.C2STerminalUpdateDisplayedItems
import net.shadowfacts.phycon.networking.ServerReceiver import net.shadowfacts.phycon.networking.ServerReceiver
/** /**
@ -23,6 +24,7 @@ object PhysicalConnectivity: ModInitializer {
PhyScreens.init() PhyScreens.init()
registerGlobalReceiver(C2STerminalRequestItem) registerGlobalReceiver(C2STerminalRequestItem)
registerGlobalReceiver(C2STerminalUpdateDisplayedItems)
} }
private fun registerGlobalReceiver(receiver: ServerReceiver) { private fun registerGlobalReceiver(receiver: ServerReceiver) {

View File

@ -2,19 +2,30 @@ package net.shadowfacts.phycon
import net.fabricmc.api.ClientModInitializer import net.fabricmc.api.ClientModInitializer
import net.fabricmc.fabric.api.blockrenderlayer.v1.BlockRenderLayerMap import net.fabricmc.fabric.api.blockrenderlayer.v1.BlockRenderLayerMap
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking
import net.fabricmc.fabric.api.client.screenhandler.v1.ScreenRegistry import net.fabricmc.fabric.api.client.screenhandler.v1.ScreenRegistry
import net.minecraft.client.render.RenderLayer import net.minecraft.client.render.RenderLayer
import net.shadowfacts.phycon.init.PhyBlocks import net.shadowfacts.phycon.init.PhyBlocks
import net.shadowfacts.phycon.init.PhyScreens import net.shadowfacts.phycon.init.PhyScreens
import net.shadowfacts.phycon.network.block.terminal.TerminalScreen import net.shadowfacts.phycon.network.block.terminal.TerminalScreen
import net.shadowfacts.phycon.networking.ClientReceiver
import net.shadowfacts.phycon.networking.S2CTerminalUpdateDisplayedItems
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
object PhysicalConnectivityClient: ClientModInitializer { object PhysicalConnectivityClient: ClientModInitializer {
override fun onInitializeClient() { override fun onInitializeClient() {
BlockRenderLayerMap.INSTANCE.putBlock(PhyBlocks.CABLE, RenderLayer.getTranslucent()) BlockRenderLayerMap.INSTANCE.putBlock(PhyBlocks.CABLE, RenderLayer.getTranslucent())
ScreenRegistry.register(PhyScreens.TERMINAL_SCREEN_HANDLER, ::TerminalScreen) ScreenRegistry.register(PhyScreens.TERMINAL_SCREEN_HANDLER, ::TerminalScreen)
registerGlobalReceiver(S2CTerminalUpdateDisplayedItems)
} }
private fun registerGlobalReceiver(receiver: ClientReceiver) {
ClientPlayNetworking.registerGlobalReceiver(receiver.CHANNEL, receiver)
}
} }

View File

@ -56,7 +56,7 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL),
private var observers = 0 private var observers = 0
val cachedNetItems = ItemStackCollections.intMap() val cachedNetItems = ItemStackCollections.intMap()
var cachedSortedNetItems = listOf<ItemStack>() // var cachedSortedNetItems = listOf<ItemStack>()
var netItemObserver: WeakReference<NetItemObserver>? = null var netItemObserver: WeakReference<NetItemObserver>? = null
@ -86,14 +86,12 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL),
private fun handleReadInventory(packet: ReadInventoryPacket) { private fun handleReadInventory(packet: ReadInventoryPacket) {
inventoryCache[packet.source] = packet.inventory inventoryCache[packet.source] = packet.inventory
updateNetItems() updateAndSync()
sync()
} }
private fun handleDeviceRemoved(packet: DeviceRemovedPacket) { private fun handleDeviceRemoved(packet: DeviceRemovedPacket) {
inventoryCache.remove(packet.source) inventoryCache.remove(packet.source)
updateNetItems() updateAndSync()
sync()
} }
private fun handleStackLocation(packet: StackLocationPacket) { private fun handleStackLocation(packet: StackLocationPacket) {
@ -113,12 +111,17 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL),
// this happens outside the normal update loop because by receiving the item stack packet // this happens outside the normal update loop because by receiving the item stack packet
// we "know" how much the count in the source inventory has changed // we "know" how much the count in the source inventory has changed
updateNetItems() updateAndSync()
sync()
return remaining return remaining
} }
private fun updateAndSync() {
updateNetItems()
sync()
netItemObserver?.get()?.netItemsChanged()
}
private fun updateNetItems() { private fun updateNetItems() {
cachedNetItems.clear() cachedNetItems.clear()
for (inventory in inventoryCache.values) { for (inventory in inventoryCache.values) {
@ -127,12 +130,12 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL),
cachedNetItems.mergeInt(stack, amount) { a, b -> a + b } cachedNetItems.mergeInt(stack, amount) { a, b -> a + b }
} }
} }
// todo: is the map necessary or is just the sorted list enough? // // todo: is the map necessary or is just the sorted list enough?
cachedSortedNetItems = cachedNetItems.object2IntEntrySet().sortedByDescending { it.intValue }.map { // cachedSortedNetItems = cachedNetItems.object2IntEntrySet().sortedByDescending { it.intValue }.map {
val stack = it.key.copy() // val stack = it.key.copy()
stack.count = it.intValue // stack.count = it.intValue
stack // stack
} // }
} }
private fun beginInsertions() { private fun beginInsertions() {
@ -175,21 +178,15 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL),
finishTimedOutPendingInsertions() finishTimedOutPendingInsertions()
} }
if (observers > 0) { if (observers > 0 && !world!!.isClient) {
if (world!!.isClient) { updateAndSync()
println(cachedNetItems)
} else {
updateNetItems()
sync()
}
} }
} }
} }
fun onActivate(player: PlayerEntity) { fun onActivate(player: PlayerEntity) {
if (!world!!.isClient) { if (!world!!.isClient) {
updateNetItems() updateAndSync()
sync()
inventoryCache.clear() inventoryCache.clear()
sendPacket(RequestInventoryPacket(ipAddress)) sendPacket(RequestInventoryPacket(ipAddress))
@ -238,8 +235,7 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL),
internalBuffer.setStack(insertion.bufferSlot, remaining) internalBuffer.setStack(insertion.bufferSlot, remaining)
// as with extracting, we "know" the new amounts and so can update instantly without actually sending out packets // as with extracting, we "know" the new amounts and so can update instantly without actually sending out packets
updateNetItems() updateAndSync()
sync()
return remaining return remaining
} }
@ -263,31 +259,31 @@ class TerminalBlockEntity: DeviceBlockEntity(PhyBlockEntities.TERMINAL),
override fun toClientTag(tag: CompoundTag): CompoundTag { override fun toClientTag(tag: CompoundTag): CompoundTag {
tag.put("InternalBuffer", internalBuffer.toTag()) tag.put("InternalBuffer", internalBuffer.toTag())
val list = ListTag() // val list = ListTag()
tag.put("CachedNetItems", list) // tag.put("CachedNetItems", list)
for ((stack, amount) in cachedNetItems) { // for ((stack, amount) in cachedNetItems) {
val entryTag = stack.toTag(CompoundTag()) // val entryTag = stack.toTag(CompoundTag())
entryTag.putInt("NetAmount", amount) // entryTag.putInt("NetAmount", amount)
list.add(entryTag) // list.add(entryTag)
} // }
return tag return tag
} }
override fun fromClientTag(tag: CompoundTag) { override fun fromClientTag(tag: CompoundTag) {
internalBuffer.fromTag(tag.getCompound("InternalBuffer")) internalBuffer.fromTag(tag.getCompound("InternalBuffer"))
val list = tag.getList("CachedNetItems", 10) // val list = tag.getList("CachedNetItems", 10)
cachedNetItems.clear() // cachedNetItems.clear()
for (entryTag in list) { // for (entryTag in list) {
val stack = ItemStack.fromTag(entryTag as CompoundTag) // val stack = ItemStack.fromTag(entryTag as CompoundTag)
val netAmount = entryTag.getInt("NetAmount") // val netAmount = entryTag.getInt("NetAmount")
cachedNetItems[stack] = netAmount // cachedNetItems[stack] = netAmount
} // }
netItemObserver?.get()?.netItemsChanged() // netItemObserver?.get()?.netItemsChanged()
cachedSortedNetItems = cachedNetItems.object2IntEntrySet().sortedByDescending { it.intValue }.map { // cachedSortedNetItems = cachedNetItems.object2IntEntrySet().sortedByDescending { it.intValue }.map {
val stack = it.key.copy() // val stack = it.key.copy()
stack.count = it.intValue // stack.count = it.intValue
stack // stack
} // }
} }
interface NetItemObserver { interface NetItemObserver {

View File

@ -17,6 +17,7 @@ import net.minecraft.text.LiteralText
import net.minecraft.text.Text import net.minecraft.text.Text
import net.minecraft.util.Identifier import net.minecraft.util.Identifier
import net.shadowfacts.phycon.PhysicalConnectivity import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.networking.C2STerminalUpdateDisplayedItems
import net.shadowfacts.phycon.util.SortMode import net.shadowfacts.phycon.util.SortMode
import org.lwjgl.glfw.GLFW import org.lwjgl.glfw.GLFW
import java.lang.NumberFormatException import java.lang.NumberFormatException
@ -34,6 +35,7 @@ class TerminalScreen(handler: TerminalScreenHandler, playerInv: PlayerInventory,
} }
private lateinit var searchBox: TextFieldWidget private lateinit var searchBox: TextFieldWidget
private lateinit var sortButton: SortButton
private lateinit var amountBox: TextFieldWidget private lateinit var amountBox: TextFieldWidget
private var dialogStack = ItemStack.EMPTY private var dialogStack = ItemStack.EMPTY
private var showingAmountDialog = false private var showingAmountDialog = false
@ -77,9 +79,8 @@ class TerminalScreen(handler: TerminalScreenHandler, playerInv: PlayerInventory,
searchBox.setEditableColor(0xffffff) searchBox.setEditableColor(0xffffff)
addChild(searchBox) addChild(searchBox)
val sortButton = SortButton(x + 256, y + 0, handler.sortMode, { sortButton = SortButton(x + 256, y + 0, handler.sortMode, {
handler.sortMode = it requestUpdatedItems()
handler.netItemsChanged()
}, ::renderTooltip) }, ::renderTooltip)
addButton(sortButton) addButton(sortButton)
@ -138,6 +139,13 @@ class TerminalScreen(handler: TerminalScreenHandler, playerInv: PlayerInventory,
doDialogRequest() doDialogRequest()
} }
dialogChildren.add(request) dialogChildren.add(request)
requestUpdatedItems()
}
private fun requestUpdatedItems() {
val player = MinecraftClient.getInstance().player!!
player.networkHandler.sendPacket(C2STerminalUpdateDisplayedItems(handler.terminal, searchBox.text, sortButton.mode))
} }
override fun tick() { override fun tick() {
@ -279,7 +287,7 @@ class TerminalScreen(handler: TerminalScreenHandler, playerInv: PlayerInventory,
val oldText = searchBox.text val oldText = searchBox.text
if (searchBox.charTyped(c, i)) { if (searchBox.charTyped(c, i)) {
if (searchBox.text != oldText) { if (searchBox.text != oldText) {
search() requestUpdatedItems()
} }
return true return true
} }
@ -307,7 +315,7 @@ class TerminalScreen(handler: TerminalScreenHandler, playerInv: PlayerInventory,
val oldText = searchBox.text val oldText = searchBox.text
if (searchBox.keyPressed(key, j, k)) { if (searchBox.keyPressed(key, j, k)) {
if (searchBox.text != oldText) { if (searchBox.text != oldText) {
search() requestUpdatedItems()
} }
return true return true
} }
@ -319,10 +327,6 @@ class TerminalScreen(handler: TerminalScreenHandler, playerInv: PlayerInventory,
} }
} }
private fun search() {
screenHandler.search(searchBox.text)
}
private fun doDialogRequest() { private fun doDialogRequest() {
showingAmountDialog = false showingAmountDialog = false
handler.requestItem(client!!.player!!, dialogStack, amountBox.intValue) handler.requestItem(client!!.player!!, dialogStack, amountBox.intValue)

View File

@ -8,12 +8,14 @@ import net.minecraft.entity.player.PlayerInventory
import net.minecraft.item.ItemStack import net.minecraft.item.ItemStack
import net.minecraft.network.PacketByteBuf import net.minecraft.network.PacketByteBuf
import net.minecraft.screen.ScreenHandler import net.minecraft.screen.ScreenHandler
import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.util.Identifier import net.minecraft.util.Identifier
import net.minecraft.util.registry.Registry import net.minecraft.util.registry.Registry
import net.shadowfacts.phycon.PhysicalConnectivity import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.init.PhyBlocks import net.shadowfacts.phycon.init.PhyBlocks
import net.shadowfacts.phycon.init.PhyScreens import net.shadowfacts.phycon.init.PhyScreens
import net.shadowfacts.phycon.networking.C2STerminalRequestItem import net.shadowfacts.phycon.networking.C2STerminalRequestItem
import net.shadowfacts.phycon.networking.S2CTerminalUpdateDisplayedItems
import net.shadowfacts.phycon.util.SortMode import net.shadowfacts.phycon.util.SortMode
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import kotlin.math.ceil import kotlin.math.ceil
@ -22,7 +24,7 @@ import kotlin.math.min
/** /**
* @author shadowfacts * @author shadowfacts
*/ */
class TerminalScreenHandler(syncId: Int, playerInv: PlayerInventory, val terminal: TerminalBlockEntity): ScreenHandler(PhyScreens.TERMINAL_SCREEN_HANDLER, syncId), class TerminalScreenHandler(syncId: Int, val playerInv: PlayerInventory, val terminal: TerminalBlockEntity): ScreenHandler(PhyScreens.TERMINAL_SCREEN_HANDLER, syncId),
TerminalBlockEntity.NetItemObserver { TerminalBlockEntity.NetItemObserver {
companion object { companion object {
@ -32,6 +34,18 @@ class TerminalScreenHandler(syncId: Int, playerInv: PlayerInventory, val termina
private val fakeInv = FakeInventory(this) private val fakeInv = FakeInventory(this)
private var searchQuery: String = "" private var searchQuery: String = ""
var sortMode = SortMode.COUNT_HIGH_FIRST var sortMode = SortMode.COUNT_HIGH_FIRST
private set
private var itemEntries = listOf<Entry>()
set(value) {
field = value
if (terminal.world!!.isClient) {
itemsForDisplay = value.map {
val stack = it.stack.copy()
stack.count = it.amount
stack
}
}
}
var itemsForDisplay = listOf<ItemStack>() var itemsForDisplay = listOf<ItemStack>()
private set private set
@ -39,8 +53,10 @@ class TerminalScreenHandler(syncId: Int, playerInv: PlayerInventory, val termina
this(syncId, playerInv, PhyBlocks.TERMINAL.getBlockEntity(playerInv.player.world, buf.readBlockPos())!!) this(syncId, playerInv, PhyBlocks.TERMINAL.getBlockEntity(playerInv.player.world, buf.readBlockPos())!!)
init { init {
terminal.netItemObserver = WeakReference(this) if (!terminal.world!!.isClient) {
netItemsChanged() terminal.netItemObserver = WeakReference(this)
netItemsChanged()
}
// network // network
for (y in 0 until 6) { for (y in 0 until 6) {
@ -69,6 +85,9 @@ class TerminalScreenHandler(syncId: Int, playerInv: PlayerInventory, val termina
} }
override fun netItemsChanged() { override fun netItemsChanged() {
val player = playerInv.player
assert(player is ServerPlayerEntity)
val filtered = terminal.cachedNetItems.object2IntEntrySet().filter { val filtered = terminal.cachedNetItems.object2IntEntrySet().filter {
if (searchQuery.isBlank()) return@filter true if (searchQuery.isBlank()) return@filter true
if (searchQuery.startsWith('@')) { if (searchQuery.startsWith('@')) {
@ -88,18 +107,25 @@ class TerminalScreenHandler(syncId: Int, playerInv: PlayerInventory, val termina
SortMode.ALPHABETICAL -> filtered.sortedBy { it.key.name.string } SortMode.ALPHABETICAL -> filtered.sortedBy { it.key.name.string }
} }
itemsForDisplay = sorted.map { itemEntries = sorted.map { Entry(it.key, it.intValue) }
val stack = it.key.copy()
stack.count = it.intValue (player as ServerPlayerEntity).networkHandler.sendPacket(S2CTerminalUpdateDisplayedItems(terminal, itemEntries, searchQuery, sortMode))
stack
}
} }
fun search(query: String) { fun sendUpdatedItemsToClient(player: ServerPlayerEntity, query: String, sortMode: SortMode) {
searchQuery = query this.searchQuery = query
this.sortMode = sortMode
netItemsChanged() netItemsChanged()
} }
fun receivedUpdatedItemsFromServer(entries: List<Entry>, query: String, sortMode: SortMode) {
assert(playerInv.player.world.isClient)
this.searchQuery = query
this.sortMode = sortMode
itemEntries = entries
}
override fun canUse(player: PlayerEntity): Boolean { override fun canUse(player: PlayerEntity): Boolean {
return true return true
} }
@ -214,4 +240,6 @@ class TerminalScreenHandler(syncId: Int, playerInv: PlayerInventory, val termina
fun isNetworkSlot(id: Int) = id in 0 until bufferSlotsStart fun isNetworkSlot(id: Int) = id in 0 until bufferSlotsStart
fun isBufferSlot(id: Int) = id in bufferSlotsStart until playerSlotsStart fun isBufferSlot(id: Int) = id in bufferSlotsStart until playerSlotsStart
fun isPlayerSlot(id: Int) = id >= playerSlotsStart fun isPlayerSlot(id: Int) = id >= playerSlotsStart
data class Entry(val stack: ItemStack, val amount: Int)
} }

View File

@ -0,0 +1,50 @@
package net.shadowfacts.phycon.networking
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking
import net.fabricmc.fabric.api.networking.v1.PacketByteBufs
import net.fabricmc.fabric.api.networking.v1.PacketSender
import net.minecraft.network.Packet
import net.minecraft.network.PacketByteBuf
import net.minecraft.server.MinecraftServer
import net.minecraft.server.network.ServerPlayNetworkHandler
import net.minecraft.server.network.ServerPlayerEntity
import net.minecraft.util.Identifier
import net.shadowfacts.phycon.PhysicalConnectivity
import net.shadowfacts.phycon.network.block.terminal.TerminalBlockEntity
import net.shadowfacts.phycon.network.block.terminal.TerminalScreenHandler
import net.shadowfacts.phycon.util.SortMode
/**
* @author shadowfacts
*/
object C2STerminalUpdateDisplayedItems: ServerReceiver {
override val CHANNEL = Identifier(PhysicalConnectivity.MODID, "terminal_update_displayed")
operator fun invoke(terminal: TerminalBlockEntity, query: String, sortMode: SortMode): Packet<*> {
val buf = PacketByteBufs.create()
buf.writeIdentifier(terminal.world!!.registryKey.value)
buf.writeBlockPos(terminal.pos)
buf.writeString(query)
buf.writeVarInt(sortMode.ordinal)
return ClientPlayNetworking.createC2SPacket(CHANNEL, buf)
}
override fun receive(server: MinecraftServer, player: ServerPlayerEntity, handler: ServerPlayNetworkHandler, buf: PacketByteBuf, responseSender: PacketSender) {
val dimID = buf.readIdentifier()
val pos = buf.readBlockPos()
val query = buf.readString()
val sortMode = SortMode.values()[buf.readVarInt()]
server.execute {
if (player.world.registryKey.value != dimID) return@execute
val screenHandler = player.currentScreenHandler
if (screenHandler !is TerminalScreenHandler) return@execute
if (screenHandler.terminal.pos != pos) return@execute
screenHandler.sendUpdatedItemsToClient(player, query, sortMode)
}
}
}

View File

@ -0,0 +1,11 @@
package net.shadowfacts.phycon.networking
import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking
import net.minecraft.util.Identifier
/**
* @author shadowfacts
*/
interface ClientReceiver: ClientPlayNetworking.PlayChannelHandler {
val CHANNEL: Identifier
}

View File

@ -0,0 +1,57 @@
package net.shadowfacts.phycon.networking
import net.fabricmc.fabric.api.networking.v1.PacketByteBufs
import net.fabricmc.fabric.api.networking.v1.PacketSender
import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking
import net.minecraft.client.MinecraftClient
import net.minecraft.client.network.ClientPlayNetworkHandler
import net.minecraft.network.Packet
import net.minecraft.network.PacketByteBuf
import net.shadowfacts.phycon.network.block.terminal.TerminalBlockEntity
import net.shadowfacts.phycon.network.block.terminal.TerminalScreenHandler
import net.shadowfacts.phycon.util.SortMode
/**
* @author shadowfacts
*/
object S2CTerminalUpdateDisplayedItems: ClientReceiver {
override val CHANNEL = C2STerminalUpdateDisplayedItems.CHANNEL
operator fun invoke(terminal: TerminalBlockEntity, entries: List<TerminalScreenHandler.Entry>, query: String, sortMode: SortMode): Packet<*> {
val buf = PacketByteBufs.create()
buf.writeIdentifier(terminal.world!!.registryKey.value)
buf.writeBlockPos(terminal.pos)
buf.writeVarInt(entries.size)
for (e in entries) {
buf.writeItemStack(e.stack)
buf.writeVarInt(e.amount)
}
buf.writeString(query)
buf.writeVarInt(sortMode.ordinal)
return ServerPlayNetworking.createS2CPacket(CHANNEL, buf)
}
override fun receive(client: MinecraftClient, handler: ClientPlayNetworkHandler, buf: PacketByteBuf, responseSender: PacketSender) {
val dimID = buf.readIdentifier()
val pos = buf.readBlockPos()
val entryCount = buf.readVarInt()
val entries = ArrayList<TerminalScreenHandler.Entry>(entryCount)
for (i in 0 until entryCount) {
entries.add(TerminalScreenHandler.Entry(buf.readItemStack(), buf.readVarInt()))
}
val query = buf.readString()
val sortMode = SortMode.values()[buf.readVarInt()]
client.execute {
if (client.player!!.world.registryKey.value != dimID) return@execute
val screenHandler = client.player!!.currentScreenHandler
if (screenHandler !is TerminalScreenHandler) return@execute
if (screenHandler.terminal.pos != pos) return@execute
screenHandler.receivedUpdatedItemsFromServer(entries, query, sortMode)
}
}
}

View File

@ -12,7 +12,7 @@ enum class SortMode {
ALPHABETICAL; ALPHABETICAL;
val prev: SortMode val prev: SortMode
get() = values()[(ordinal - 1) % values().size] get() = values()[(ordinal - 1 + values().size) % values().size]
val next: SortMode val next: SortMode
get() = values()[(ordinal + 1) % values().size] get() = values()[(ordinal + 1) % values().size]