diff --git a/li/cil/oc/Config.scala b/li/cil/oc/Config.scala index 64ada22c3..328eadc7e 100644 --- a/li/cil/oc/Config.scala +++ b/li/cil/oc/Config.scala @@ -1,6 +1,7 @@ package li.cil.oc import java.io.File +import li.cil.oc.util.ExtendedConfiguration._ import li.cil.oc.util.PackedColor object Config { @@ -67,7 +68,7 @@ object Config { // server.filesystem var fileCost = 512 - var filesBuffered = true + var bufferChanges = true var maxHandles = 16 var maxReadBuffer = 8 * 1024 @@ -84,6 +85,8 @@ object Config { var canAttackPlayers = false var canPlaceInAir = false var itemDamageRate = 0.05 + var swingRange = 0.49 + var useAndPlaceRange = 0.65 // ----------------------------------------------------------------------- // // robot.delays @@ -96,7 +99,6 @@ object Config { var turnDelay = 0.4 var useDelay = 0.4 - // ----------------------------------------------------------------------- // def load(file: File) = { @@ -121,309 +123,331 @@ object Config { config.getCategory("client"). setComment("Client side settings, presentation and performance related stuff.") - maxScreenTextRenderDistance = config.get("client", "maxScreenTextRenderDistance", maxScreenTextRenderDistance, "" + - "The maximum distance at which to render text on screens. Rendering text\n" + - "can be pretty expensive, so if you have a lot of screens you'll want\n" + - "to avoid huge numbers here. Note that this setting is client-sided, and\n" + - "only has an impact on render performance on clients."). - getDouble(maxScreenTextRenderDistance) + maxScreenTextRenderDistance = config.fetch("client.maxScreenTextRenderDistance", maxScreenTextRenderDistance, + """|The maximum distance at which to render text on screens. Rendering text + |can be pretty expensive, so if you have a lot of screens you'll want + |to avoid huge numbers here. Note that this setting is client-sided, and + |only has an impact on render performance on clients.""".stripMargin) - screenTextFadeStartDistance = config.get("client", "screenTextFadeStartDistance", screenTextFadeStartDistance, "" + - "The distance at which to start fading out the text on screens. This is\n" + - "purely cosmetic, to avoid text disappearing instantly when moving too far\n" + - "away from a screen. This should have no measurable impact on performance.\n" + - "Note that this needs OpenGL 1.4 to work, otherwise text will always just\n" + - "instantly disappear when moving away from the screen displaying it."). - getDouble(screenTextFadeStartDistance) + screenTextFadeStartDistance = config.fetch("client.screenTextFadeStartDistance", screenTextFadeStartDistance, + """|The distance at which to start fading out the text on screens. This is + |purely cosmetic, to avoid text disappearing instantly when moving too far + |away from a screen. This should have no measurable impact on performance. + |Note that this needs OpenGL 1.4 to work, otherwise text will always just + |instantly disappear when moving away from the screen displaying it.""".stripMargin) - textLinearFiltering = config.get("client", "textLinearFiltering", textLinearFiltering, "" + - "Whether to apply linear filtering for text displayed on screens when the\n" + - "screen has to be scaled down - i.e. the text is rendered at a resolution\n" + - "lower than their native one, e.g. when the GUI scale is less than one or\n" + - "when looking at a far away screen. This leads to smoother text for scaled\n" + - "down text but results in characters not perfectly connecting anymore (for\n" + - "example for box drawing characters. Look it up on Wikipedia.)"). - getBoolean(textLinearFiltering) + textLinearFiltering = config.fetch("client.textLinearFiltering", textLinearFiltering, + """|Whether to apply linear filtering for text displayed on screens when the + |screen has to be scaled down - i.e. the text is rendered at a resolution + |lower than their native one, e.g. when the GUI scale is less than one or + |when looking at a far away screen. This leads to smoother text for scaled + |down text but results in characters not perfectly connecting anymore (for + |example for box drawing characters. Look it up on Wikipedia.)""".stripMargin) // --------------------------------------------------------------------- // config.getCategory("power"). setComment("Power settings, buffer sizes and power consumption.") - ignorePower = config.get("power", "ignorePower", ignorePower, "" + - "Whether to ignore any power requirements. Whenever something requires\n" + - "power to function, it will try to get the amount of energy it needs from\n" + - "the buffer of its connector node, and in case it fails it won't perform\n" + - "the action / trigger a shutdown / whatever. Setting this to `true` will\n" + - "simply make the check 'is there enough energy' succeed unconditionally.\n" + - "Note that buffers are still filled and emptied following the usual rules,\n" + - "there just is no failure case anymore."). - getBoolean(ignorePower) + ignorePower = config.fetch("power.ignorePower", ignorePower, + """|Whether to ignore any power requirements. Whenever something requires + |power to function, it will try to get the amount of energy it needs from + |the buffer of its connector node, and in case it fails it won't perform + |the action / trigger a shutdown / whatever. Setting this to `true` will + |simply make the check 'is there enough energy' succeed unconditionally. + |Note that buffers are still filled and emptied following the usual rules, + |there just is no failure case anymore.""".stripMargin) // --------------------------------------------------------------------- // - bufferCapacitor = config.get("power.buffer", "bufferCapacitor", bufferCapacitor, "" + - "The amount of energy a capacitor can store."). - getDouble(bufferCapacitor) max 0 + bufferCapacitor = config.fetch("power.buffer.capacitor", bufferCapacitor, + """|The amount of energy a capacitor can store.""".stripMargin) max 0 - bufferConverter = config.get("power.buffer", "bufferConverter", bufferConverter, "" + - "The amount of energy a power converter can store."). - getDouble(bufferConverter) max 0 + bufferConverter = config.fetch("power.buffer.converter", bufferConverter, + """|The amount of energy a power converter can store.""".stripMargin) max 0 - bufferPowerSupply = config.get("power.buffer", "bufferPowerSupply", bufferPowerSupply, "" + - "The amount of energy a power supply can store."). - getDouble(bufferPowerSupply) max 0 + bufferPowerSupply = config.fetch("power.buffer.powerSupply", bufferPowerSupply, + """|The amount of energy a power supply can store.""".stripMargin) max 0 // --------------------------------------------------------------------- // - computerCost = config.get("power.cost", "computerCost", computerCost, "" + - "The amount of energy a computer consumes per tick when running."). - getDouble(computerCost) max 0 + computerCost = config.fetch("power.cost.computer", computerCost, + """|The amount of energy a computer consumes per tick when running.""".stripMargin) max 0 - gpuFillCost = config.get("power.cost", "gpuFillCost", gpuFillCost, "" + - "Energy it takes to change a single 'pixel' via the fill command. This\n" + - "means the total cost of the fill command will be its area times this."). - getDouble(gpuFillCost) max 0 + gpuFillCost = config.fetch("power.cost.gpuFill", gpuFillCost, + """|Energy it takes to change a single 'pixel' via the fill command. This + |means the total cost of the fill command will be its area times this.""".stripMargin) max 0 - gpuClearCost = config.get("power.cost", "gpuClearCost", gpuClearCost, "" + - "Energy it takes to change a single 'pixel' to blank using the fill\n" + - "command. This means the total cost of the fill command will be its\n" + - "area times this."). - getDouble(gpuClearCost) max 0 + gpuClearCost = config.fetch("power.cost.gpuClear", gpuClearCost, + """|Energy it takes to change a single 'pixel' to blank using the fill + |command. This means the total cost of the fill command will be its + |area times this.""".stripMargin) max 0 - gpuCopyCost = config.get("power.cost", "gpuCopyCost", gpuCopyCost, "" + - "Energy it takes to move a single 'pixel' via the copy command. This\n" + - "means the total cost of the copy command will be its area times this."). - getDouble(gpuCopyCost) max 0 + gpuCopyCost = config.fetch("power.cost.gpuCopy", gpuCopyCost, + """|Energy it takes to move a single 'pixel' via the copy command. This + |means the total cost of the copy command will be its area times this.""".stripMargin) max 0 - gpuSetCost = config.get("power.cost", "gpuSetCost", gpuSetCost, "" + - "Energy it takes to change a single 'pixel' via the set command. For\n" + - "calls to set with a string, this means the total cost will be the\n" + - "string length times this."). - getDouble(gpuSetCost) max 0 + gpuSetCost = config.fetch("power.cost.gpuSet", gpuSetCost, + """|Energy it takes to change a single 'pixel' via the set command. For + |calls to set with a string, this means the total cost will be the + |string length times this.""".stripMargin) max 0 - hddReadCost = config.get("power.cost", "hddReadCost", hddReadCost, "" + - "Energy it takes read a single byte from a file system. Note that non\n" + - "I/O operations on file systems such as `list` or `getFreeSpace` do\n" + - "*not* consume power."). - getDouble(hddReadCost) max 0 + hddReadCost = config.fetch("power.cost.hddRead", hddReadCost, + """|Energy it takes read a single byte from a file system. Note that non + |I/O operations on file systems such as `list` or `getFreeSpace` do + |*not* consume power.""".stripMargin) max 0 - hddWriteCost = config.get("power.cost", "hddWriteCost", hddWriteCost, "" + - "Energy it takes to write a single byte to a file system."). - getDouble(hddWriteCost) max 0 + hddWriteCost = config.fetch("power.cost.hddWrite", hddWriteCost, + """|Energy it takes to write a single byte to a file system.""".stripMargin) max 0 - powerSupplyCost = config.get("power.cost", "powerSupplyCost", powerSupplyCost, "" + - "The amount of energy a power supply (item) produces per tick. This is\n" + - "basically just a consumer, but instead of taking energy it puts it\n" + - "back into the network. This is slightly more than what a computer\n" + - "consumes per tick. It's meant as an easy way to powering a small\n" + - "setup, mostly for testing. "). - getDouble(powerSupplyCost) + powerSupplyCost = config.fetch("power.cost.powerSupply", powerSupplyCost, + """|The amount of energy a power supply (item) produces per tick. This is + |basically just a consumer, but instead of taking energy it puts it + |back into the network. This is slightly more than what a computer + |consumes per tick. It's meant as an easy way to powering a small + |setup, mostly for testing.""".stripMargin) - screenCost = config.get("power.cost", "screenCost", screenCost, "" + - "The amount of energy a screen consumes per tick. If a screen cannot\n" + - "consume the defined amount of energy it will stop rendering the text\n" + - "that should be displayed on it. It will *not* forget that text,\n" + - "however, so when enough power is available again it will restore the\n" + - "previously displayed text (with any changes possibly made in the\n" + - "meantime). Note that for multi-block screens *each* screen that is\n" + - "part of it will consume this amount of energy per tick."). - getDouble(screenCost) max 0 + screenCost = config.fetch("power.cost.screen", screenCost, + """|The amount of energy a screen consumes per displayed character per + |tick. If a screen cannot consume the defined amount of energy it will + |stop rendering the text that should be displayed on it. It will *not* + |forget that text, however, so when enough power is available again it + |will restore the previously displayed text (with any changes possibly + |made in the meantime). Note that for multi-block screens *each* + |screen that is part of it will consume this amount of energy per + |tick.""".stripMargin) max 0 - wirelessCostPerRange = config.get("power.cost", "wirelessCostPerRange", wirelessCostPerRange, "" + - "The amount of energy it costs to send a signal with strength one,\n" + - "which means the signal reaches one block. This is scaled up linearly,\n" + - "so for example to send a signal 400 blocks a signal strength of 400\n" + - "is required, costing a total of 400 * `wirelessCostPerRange`. In\n" + - "other words, the higher this value, the higher the cost of wireless\n" + - "messages.\n" + - "See also: `maxWirelessRange`."). - getDouble(wirelessCostPerRange) max 0 + wirelessCostPerRange = config.fetch("power.cost.wirelessStrength", wirelessCostPerRange, + """|The amount of energy it costs to send a signal with strength one, + |which means the signal reaches one block. This is scaled up linearly, + |so for example to send a signal 400 blocks a signal strength of 400 + |is required, costing a total of 400 * `wirelessCostPerRange`. In + |other words, the higher this value, the higher the cost of wireless + |messages.[nl] + |See also: `maxWirelessRange`.""".stripMargin) max 0 // --------------------------------------------------------------------- // config.getCategory("server"). setComment("Server side settings, gameplay and security related stuff.") - baseMemory = config.get("server.computer", "baseMemory", baseMemory, "" + - "The base amount of memory made available in computers even if they\n" + - "have no RAM installed. Use this if you feel you can't get enough RAM\n" + - "using the given means, that being RAM components. Just keep in mind\n" + - "that this is global and applies to all computers!"). - getInt(baseMemory) + baseMemory = config.fetch("server.computer.baseMemory", baseMemory, + """|The base amount of memory made available in computers even if they + |have no RAM installed. Use this if you feel you can't get enough RAM + |using the given means, that being RAM components. Just keep in mind + |that this is global and applies to all computers!""".stripMargin) - canComputersBeOwned = config.get("server.computer", "canComputersBeOwned", canComputersBeOwned, "" + - "This determines whether computers can only be used by players that\n" + - "are registered as users on them. Per default a newly placed computer\n" + - "has no users. Whenever there are no users the computer is free for\n" + - "all. Users can be managed via the Lua API (os.addUser, os.removeUser,\n" + - "os.users). If this is true, the following interactions are only\n" + - "possible for users:\n" + - " - input via the keyboard.\n" + - " - inventory management.\n" + - " - breaking the computer block.\n" + - "If this is set to false, all computers will always be usable by all\n" + - "players, no matter the contents of the user list. Note that operators\n" + - "are treated as if they were in the user list of every computer, i.e.\n" + - "no restrictions apply to them.\n" + - "See also: `maxUsers` and `maxUsernameLength`."). - getBoolean(canComputersBeOwned) + canComputersBeOwned = config.fetch("server.computer.canComputersBeOwned", canComputersBeOwned, + """|This determines whether computers can only be used by players that + |are registered as users on them. Per default a newly placed computer + |has no users. Whenever there are no users the computer is free for + |all. Users can be managed via the Lua API (os.addUser, os.removeUser, + |os.users). If this is true, the following interactions are only + |possible for users:[nl] + | - input via the keyboard.[nl] + | - inventory management.[nl] + | - breaking the computer block.[nl] + |If this is set to false, all computers will always be usable by all + |players, no matter the contents of the user list. Note that operators + |are treated as if they were in the user list of every computer, i.e. + |no restrictions apply to them.[nl] + |See also: `maxUsers` and `maxUsernameLength`.""".stripMargin) - maxUsernameLength = config.get("server.computer", "maxUsernameLength", maxUsernameLength, "" + - "Sanity check for username length for users registered with computers.\n" + - "We store the actual user names instead of a hash to allow iterating\n" + - "the list of registered users on the Lua side.\n" + - "See also: `canComputersBeOwned`."). - getInt(maxUsernameLength) max 0 + maxUsernameLength = config.fetch("server.computer.maxUsernameLength", maxUsernameLength, + """|Sanity check for username length for users registered with computers. + |We store the actual user names instead of a hash to allow iterating + |the list of registered users on the Lua side.[nl] + |See also: `canComputersBeOwned`.""".stripMargin) max 0 - maxUsers = config.get("server.computer", "maxUsers", maxUsers, "" + - "The maximum number of users that can be registered with a single\n" + - "computer. This is used to avoid computers allocating unchecked\n" + - "amounts of memory by registering an unlimited number of users.\n" + - "See also: `canComputersBeOwned`."). - getInt(maxUsers) max 0 + maxUsers = config.fetch("server.computer.maxUsers", maxUsers, + """|The maximum number of users that can be registered with a single + |computer. This is used to avoid computers allocating unchecked + |amounts of memory by registering an unlimited number of users. + |See also: `canComputersBeOwned`.""".stripMargin) max 0 - startupDelay = config.get("server.computer", "startupDelay", startupDelay, "" + - "The time in seconds to wait after a computer has been restored before\n" + - "it continues to run. This is meant to allow the world around the\n" + - "computer to settle, avoiding issues such as components in neighboring\n" + - "chunks being removed and then re-connected and other odd things that\n" + - "might happen."). - getDouble(startupDelay) max 0 + startupDelay = config.fetch("server.computer.startupDelay", startupDelay, + """|The time in seconds to wait after a computer has been restored before + |it continues to run. This is meant to allow the world around the + |computer to settle, avoiding issues such as components in neighboring + |chunks being removed and then re-connected and other odd things that + |might happen.""".stripMargin) max 0 - threads = config.get("server.computer", "threads", threads, "" + - "The overall number of threads to use to drive computers. Whenever a\n" + - "computer should run, for example because a signal should be processed\n" + - "or some sleep timer expired it is queued for execution by a worker\n" + - "thread. The higher the number of worker threads, the less likely it\n" + - "will be that computers block each other from running, but the higher\n" + - "the host system's load may become."). - getInt(threads) max 1 + threads = config.fetch("server.computer.threads", threads, + """|The overall number of threads to use to drive computers. Whenever a + |computer should run, for example because a signal should be processed + |or some sleep timer expired it is queued for execution by a worker + |thread. The higher the number of worker threads, the less likely it + |will be that computers block each other from running, but the higher + |the host system's load may become.""".stripMargin) max 1 - timeout = config.get("server.computer", "timeout", timeout, "" + - "The time in seconds a program may run without yielding before it is\n" + - "forcibly aborted. This is used to avoid stupidly written or malicious\n" + - "programs blocking other computers by locking down the executor\n" + - "threads. Note that changing this won't have any effect on computers\n" + - "that are already running - they'll have to be rebooted for this to\n" + - "take effect."). - getDouble(timeout) max 0 + timeout = config.fetch("server.computer.timeout", timeout, + """|The time in seconds a program may run without yielding before it is + |forcibly aborted. This is used to avoid stupidly written or malicious + |programs blocking other computers by locking down the executor + |threads. Note that changing this won't have any effect on computers + |that are already running - they'll have to be rebooted for this to + |take effect.""".stripMargin) max 0 // --------------------------------------------------------------------- // - fileCost = config.get("server.filesystem", "fileCost", fileCost, "" + - "The base 'cost' of a single file or directory on a limited file\n" + - "system, such as hard drives. When computing the used space we add\n" + - "this cost to the real size of each file (and folders, which are zero\n" + - "sized otherwise). This is to ensure that users cannot spam the file\n" + - "system with an infinite number of files and/or folders. Note that the\n" + - "size returned via the API will always be the real file size, however."). - getInt(fileCost) max 0 + fileCost = config.fetch("server.filesystem.fileCost", fileCost, + """|The base 'cost' of a single file or directory on a limited file + |system, such as hard drives. When computing the used space we add + |this cost to the real size of each file (and folders, which are zero + |sized otherwise). This is to ensure that users cannot spam the file + |system with an infinite number of files and/or folders. Note that the + |size returned via the API will always be the real file size, however.""".stripMargin) max 0 - filesBuffered = config.get("server.filesystem", "filesBuffered", filesBuffered, "" + - "Whether persistent file systems such as disk drivers should be\n" + - "'buffered', and only written to disk when the world is saved. This\n" + - "applies to all hard drives. The advantage of having this enabled is\n" + - "that data will never go 'out of sync' with the computer's state if\n" + - "the game crashes. The price is slightly higher memory consumption,\n" + - "since all loaded files have to be kept in memory (loaded as in when\n" + - "the hard drive is in a computer)."). - getBoolean(filesBuffered) + bufferChanges = config.fetch("server.filesystem.bufferChanges", bufferChanges, + """|Whether persistent file systems such as disk drivers should be + |'buffered', and only written to disk when the world is saved. This + |applies to all hard drives. The advantage of having this enabled is + |that data will never go 'out of sync' with the computer's state if + |the game crashes. The price is slightly higher memory consumption, + |since all loaded files have to be kept in memory (loaded as in when + |the hard drive is in a computer).""".stripMargin) - maxHandles = config.get("server.filesystem", "maxHandles", maxHandles, "" + - "The maximum number of file handles any single computer may have open\n" + - "at a time. Note that this is *per filesystem*. Also note that this is\n" + - "only enforced by the filesystem node - if an add-on decides to be\n" + - "fancy it may well ignore this. Since file systems are usually\n" + - "'virtual' this will usually not have any real impact on performance\n" + - "and won't be noticeable on the host operating system.") - .getInt(maxHandles) max 0 + maxHandles = config.fetch("server.filesystem.maxHandles", maxHandles, + """|The maximum number of file handles any single computer may have open + |at a time. Note that this is *per filesystem*. Also note that this is + |only enforced by the filesystem node - if an add-on decides to be + |fancy it may well ignore this. Since file systems are usually + |'virtual' this will usually not have any real impact on performance + |and won't be noticeable on the host operating system.""".stripMargin) max 0 - maxReadBuffer = config.get("server.filesystem", "maxReadBuffer", maxReadBuffer, "" + - "The maximum block size that can be read in one 'read' call on a file\n" + - "system. This is used to limit the amount of memory a call from a user\n" + - "program can cause to be allocated on the host side: when 'read' is,\n" + - "called a byte array with the specified size has to be allocated. So\n" + - "if this weren't limited, a Lua program could trigger massive memory\n" + - "allocations regardless of the amount of RAM installed in the computer\n" + - "it runs on. As a side effect this pretty much determines the read\n" + - "performance of file systems.") - .getInt(maxReadBuffer) max 0 + maxReadBuffer = config.fetch("server.filesystem.maxReadBuffer", maxReadBuffer, + """|The maximum block size that can be read in one 'read' call on a file + |system. This is used to limit the amount of memory a call from a user + |program can cause to be allocated on the host side: when 'read' is, + |called a byte array with the specified size has to be allocated. So + |if this weren't limited, a Lua program could trigger massive memory + |allocations regardless of the amount of RAM installed in the computer + |it runs on. As a side effect this pretty much determines the read + |performance of file systems.""".stripMargin) max 0 // --------------------------------------------------------------------- // - commandUser = config.get("server.misc", "commandUser", commandUser, "" + - "The user name to specify when executing a command via a command\n" + - "block. If you leave this empty it will use the address of the network\n" + - "node that sent the execution request - which will usually be a\n" + - "computer."). - getString.trim + commandUser = config.fetch("server.misc.commandUser", commandUser, + """|The user name to specify when executing a command via a command + |block. If you leave this empty it will use the address of the network + |node that sent the execution request - which will usually be a + |computer.""".stripMargin).trim - maxScreenHeight = config.get("server.misc", "maxScreenHeight", maxScreenHeight, "" + - "The maximum height of multi-block screens, in blocks. This is limited\n" + - "to avoid excessive computations for merging screens. If you really\n" + - "need bigger screens it's probably safe to bump this quite a bit\n" + - "before you notice anything, since at least incremental updates should\n" + - "be very efficient (i.e. when adding/removing a single screen).") - .getInt(maxScreenHeight) max 1 + maxScreenHeight = config.fetch("server.misc.maxScreenHeight", maxScreenHeight, + """|The maximum height of multi-block screens, in blocks. This is limited + |to avoid excessive computations for merging screens. If you really + |need bigger screens it's probably safe to bump this quite a bit + |before you notice anything, since at least incremental updates should + |be very efficient (i.e. when adding/removing a single screen).""".stripMargin) max 1 - maxScreenWidth = config.get("server.misc", "maxScreenWidth", maxScreenWidth, "" + - "The maximum width of multi-block screens, in blocks.\n" + - "See also: `maxScreenHeight`.") - .getInt(maxScreenWidth) max 1 + maxScreenWidth = config.fetch("server.misc.maxScreenWidth", maxScreenWidth, + """|The maximum width of multi-block screens, in blocks.[nl] + |See also: `maxScreenHeight`.""".stripMargin) max 1 - maxWirelessRange = config.get("server.misc", "maxWirelessRange", maxWirelessRange, "" + - "The maximum distance a wireless message can be sent. In other words,\n" + - "this is the maximum signal strength a wireless network card supports.\n" + - "This is used to limit the search range in which to check for modems,\n" + - "which may or may not lead to performance issues for ridiculous\n" + - "ranges - like, you know, more than the loaded area.\n" + - "See also: `wirelessCostPerRange`."). - getDouble(maxWirelessRange) max 0 + maxWirelessRange = config.fetch("server.misc.maxWirelessRange", maxWirelessRange, + """|The maximum distance a wireless message can be sent. In other words, + |this is the maximum signal strength a wireless network card supports. + |This is used to limit the search range in which to check for modems, + |which may or may not lead to performance issues for ridiculous + |ranges - like, you know, more than the loaded area.[nl] + |See also: `wirelessCostPerRange`.""".stripMargin) max 0 // --------------------------------------------------------------------- // config.getCategory("robot"). setComment("Robot related settings, what they may do and general balancing.") - allowActivateBlocks = config.get("robot", "allowActivateBlocks", allowActivateBlocks, "" + - "Whether robots may 'activate' blocks in the world. This includes\n" + - "pressing buttons and flipping switches, for example. Disable this if\n" + - "it causes problems with some mod (but let me know!) or if you think\n" + - "this feature is too over-powered."). - getBoolean(allowActivateBlocks) + allowActivateBlocks = config.fetch("robot.allowActivateBlocks", allowActivateBlocks, + """|Whether robots may 'activate' blocks in the world. This includes + |pressing buttons and flipping levers, for example. Disable this if + |it causes problems with some mod (but let me know!) or if you think + |this feature is too over-powered.""".stripMargin) - canAttackPlayers = config.get("robot", "canAttackPlayers", canAttackPlayers, "" + - "Whether robots may damage players if they get in their way. This\n" + - "includes all 'player' entities, which may be more than just real\n" + - "players in the game."). - getBoolean(canAttackPlayers) + canAttackPlayers = config.fetch("robot.canAttackPlayers", canAttackPlayers, + """|Whether robots may damage players if they get in their way. This + |includes all 'player' entities, which may be more than just real + |players in the game.""".stripMargin) - canPlaceInAir = config.get("robot", "canPlaceInAir", canPlaceInAir, "" + - "Whether robots may place blocks in thin air, i.e. without a reference\n" + - "point (as is required for real players). Set this to true to emulate\n" + - "ComputerCraft's Turtles' behavior. When left false robots have to\n" + - "target an existing block face to place another block. For example,\n" + - "if the robots stands on a perfect plain, you have to call\n" + - "`robot.place(sides.down)` to place a block, instead of just\n" + - "`robot.place()`, which will default to `robot.place(sides.front)`."). - getBoolean(canPlaceInAir) + canPlaceInAir = config.fetch("robot.canPlaceInAir", canPlaceInAir, + """|Whether robots may place blocks in thin air, i.e. without a reference + |point (as is required for real players). Set this to true to emulate + |ComputerCraft's Turtles' behavior. When left false robots have to + |target an existing block face to place another block. For example, + |if the robots stands on a perfect plane, you have to call + |`robot.place(sides.down)` to place a block, instead of just + |`robot.place()`, which will default to `robot.place(sides.front)`.""".stripMargin) - itemDamageRate = config.get("robot", "itemDamageRate", itemDamageRate, "" + - "The rate at which items used as tools by robots take damage. A value\n" + - "of one means that items lose durability as quickly as when they are\n" + - "used by a real player. A value of zero means they will not lose any\n" + - "durability at all. This only applies to items that can actually be\n" + - "damaged (such as swords, pickaxes, axes and shovels).\n" + - "Note that this actually is the *chance* of an item losing durability\n" + - "when it is used. Or in other words, it's the inverse chance that the\n" + - "item will be automatically repaired for the damage it just took\n" + - "immediately after it was used."). - getDouble(itemDamageRate) max 0 min 1 + itemDamageRate = config.fetch("robot.itemDamageRate", itemDamageRate, + """|The rate at which items used as tools by robots take damage. A value + |of one means that items lose durability as quickly as when they are + |used by a real player. A value of zero means they will not lose any + |durability at all. This only applies to items that can actually be + |damaged (such as swords, pickaxes, axes and shovels).[nl] + |Note that this actually is the *chance* of an item losing durability + |when it is used. Or in other words, it's the inverse chance that the + |item will be automatically repaired for the damage it just took + |immediately after it was used.""".stripMargin) max 0 min 1 + + swingRange = config.fetch("robot.swingRange", swingRange, + """|The 'range' of robots when swinging an equipped tool (left click). + |This is the distance to the center of block the robot swings the + |tool in to the side the tool is swung towards. I.e. for the collision + |check, which is performed via ray tracing, this determines the end + |point of the ray like so:[nl] + |`block_center + unit_vector_towards_side * swingRange`[nl] + |This defaults to a value just below 0.5 to ensure the robots will not + |hit anything that's actually outside said block.""".stripMargin) + + useAndPlaceRange = config.fetch("robot.useAndPlaceRange", useAndPlaceRange, + """|The 'range' of robots when using an equipped tool (right click) or + |when placing items from their inventory. See `robot.swingRange`. This + |defaults to a value large enough to allow robots to detect 'farmland', + |i.e. tilled dirt, so that they can plant seeds.""".stripMargin) + + // ----------------------------------------------------------------------- // + // robot.delays + + dropDelay = config.fetch("robot.delays.drop", dropDelay, + """|The time in seconds to pause execution after an item was successfully + |dropped from a robot's inventory.""".stripMargin) max 0 + + moveDelay = config.fetch("robot.delays.move", moveDelay, + """|The time in seconds to pause execution after a robot issued a + |successful move command. Note that this essentially determines how + |fast robots can move around, since this also determines the length of + |the move animation.""".stripMargin) max 0.1 + + placeDelay = config.fetch("robot.delays.place", placeDelay, + """|The time in seconds to pause execution after a robot successfully + |placed an item from its inventory.""".stripMargin) max 0 + + suckDelay = config.fetch("robot.delays.suck", suckDelay, + """|The time in seconds to pause execution after a robot successfully + |picked up an item after triggering a suck command.""".stripMargin) max 0 + + swingDelay = config.fetch("robot.delays.swing", swingDelay, + """|The time in seconds to pause execution after a robot successfully + |swung a tool (or it's 'hands' if nothing is equipped). Successful in + |this case means that it hit something, either by attacking an entity + |or by breaking a block.""".stripMargin) max 0 + + turnDelay = config.fetch("robot.delays.turn", turnDelay, + """|The time in seconds to pause execution after a robot turned either + |left or right. Note that this essentially determines hw fast robots + |can turn around, since this also determines the length of the turn + |animation.""".stripMargin) max 0.1 + + useDelay = config.fetch("robot.delays.use", useDelay, + """|The time in seconds to pause execution after a robot successfully + |used an equipped tool (or it's 'hands' if nothing is equipped). + |Successful in this case means that it either used the equipped item, + |for example bone meal, or that it activated a block, for example by + |pushing a button.""".stripMargin) max 0 // --------------------------------------------------------------------- // - if (config.hasChanged) + if (config.hasChanged) { config.save() + } } } \ No newline at end of file diff --git a/li/cil/oc/common/block/Item.scala b/li/cil/oc/common/block/Item.scala index 640add8b9..6e99f92c8 100644 --- a/li/cil/oc/common/block/Item.scala +++ b/li/cil/oc/common/block/Item.scala @@ -17,8 +17,8 @@ class Item(id: Int) extends ItemBlock(id) { override def getUnlocalizedName = Config.namespace + "block" override def getUnlocalizedName(item: ItemStack) = - Block.blocksList(item.itemID) match { - case multiBlock: Delegator[_] => Config.namespace + "block." + multiBlock.getUnlocalizedName(item.getItemDamage) + Block.blocksList(getBlockID) match { + case delegator: Delegator[_] => Config.namespace + "block." + delegator.getUnlocalizedName(item.getItemDamage) case block => block.getUnlocalizedName } diff --git a/li/cil/oc/common/item/Delegate.scala b/li/cil/oc/common/item/Delegate.scala index a45efd3ed..218b2e3ab 100644 --- a/li/cil/oc/common/item/Delegate.scala +++ b/li/cil/oc/common/item/Delegate.scala @@ -33,6 +33,6 @@ trait Delegate { def registerIcons(iconRegister: IconRegister) {} - def equals(item: ItemStack) = - item != null && item.itemID == parent.itemID && parent.subItem(item).exists(_.itemId == itemId) + def equals(stack: ItemStack) = + stack != null && stack.getItem == parent && parent.subItem(stack).exists(_ == this) } diff --git a/li/cil/oc/common/item/Delegator.scala b/li/cil/oc/common/item/Delegator.scala index 012ca1f7f..27cff5d76 100644 --- a/li/cil/oc/common/item/Delegator.scala +++ b/li/cil/oc/common/item/Delegator.scala @@ -29,7 +29,7 @@ class Delegator(id: Int) extends Item(id) { def subItem(item: ItemStack): Option[Delegate] = subItem(item.getItemDamage) match { - case Some(subItem) if item.itemID == this.itemID => Some(subItem) + case Some(subItem) if item.getItem == this => Some(subItem) case _ => None } diff --git a/li/cil/oc/server/component/Robot.scala b/li/cil/oc/server/component/Robot.scala index 03ed2a49d..0330b42d4 100644 --- a/li/cil/oc/server/component/Robot.scala +++ b/li/cil/oc/server/component/Robot.scala @@ -150,7 +150,7 @@ class Robot(val robot: tileentity.Robot) extends Computer(robot) { } else { player.setSneaking(sneaky) - val what = Option(pick(facing, side, 0.51)) match { + val what = Option(pick(facing, side, Config.useAndPlaceRange)) match { case Some(hit) if hit.typeOfHit == EnumMovingObjectType.TILE => val (bx, by, bz, hx, hy, hz) = clickParamsFromHit(hit) player.placeBlock(stack, bx, by, bz, hit.sideHit, hx, hy, hz) @@ -207,7 +207,7 @@ class Robot(val robot: tileentity.Robot) extends Computer(robot) { throw new IllegalArgumentException("invalid side") } val player = robot.player(facing, side) - Option(pick(facing, side, 0.49)) match { + Option(pick(facing, side, Config.swingRange)) match { case Some(hit) => val what = hit.typeOfHit match { case EnumMovingObjectType.ENTITY => @@ -257,11 +257,12 @@ class Robot(val robot: tileentity.Robot) extends Computer(robot) { case _ => result(false) } player.setSneaking(sneaky) - val what = Option(pick(facing, side, 0.51)) match { + val what = Option(pick(facing, side, Config.useAndPlaceRange)) match { case Some(hit) => hit.typeOfHit match { case EnumMovingObjectType.ENTITY => // TODO Is there any practical use for this? Most of the stuff related to this is still 'obfuscated'... + // TODO I think this is used for shearing sheep, for example... needs looking into. result(false, "entity") case EnumMovingObjectType.TILE => val (bx, by, bz, hx, hy, hz) = clickParamsFromHit(hit) @@ -380,7 +381,7 @@ class Robot(val robot: tileentity.Robot) extends Computer(robot) { // ----------------------------------------------------------------------- // private def haveSameItemType(stackA: ItemStack, stackB: ItemStack) = - stackA.itemID == stackB.itemID && + stackA.getItem == stackB.getItem && (!stackA.getHasSubtypes || stackA.getItemDamage == stackB.getItemDamage) private def stackInSlot(slot: Int) = Option(robot.getStackInSlot(actualSlot(slot))) diff --git a/li/cil/oc/server/component/robot/Player.scala b/li/cil/oc/server/component/robot/Player.scala index 60e4c3e80..c748e9819 100644 --- a/li/cil/oc/server/component/robot/Player.scala +++ b/li/cil/oc/server/component/robot/Player.scala @@ -12,7 +12,7 @@ import net.minecraft.potion.PotionEffect import net.minecraft.server.MinecraftServer import net.minecraft.util.{Vec3, AxisAlignedBB, DamageSource, ChunkCoordinates} import net.minecraft.world.World -import net.minecraftforge.common.{ForgeDirection, FakePlayer} +import net.minecraftforge.common.{ForgeHooks, MinecraftForge, ForgeDirection, FakePlayer} import net.minecraftforge.event.entity.player.PlayerInteractEvent.Action import net.minecraftforge.event.{Event, ForgeEventFactory} import net.minecraftforge.fluids.FluidRegistry @@ -179,6 +179,7 @@ class Player(val robot: Robot) extends FakePlayer(robot.world, "OpenComputers") val blockId = world.getBlockId(x, y, z) val block = Block.blocksList(blockId) + val metadata = world.getBlockMetadata(x, y, z) val mayBreakBlock = event.useBlock != Event.Result.DENY && blockId > 0 && block != null val canBreakBlock = mayBreakBlock && !block.isAirBlock(world, x, y, z) && @@ -192,12 +193,15 @@ class Player(val robot: Robot) extends FakePlayer(robot.world, "OpenComputers") return false } + if (!ForgeHooks.canHarvestBlock(block, this, metadata)) { + return false + } + val stack = getCurrentEquippedItem if (stack != null && stack.getItem.onBlockStartBreak(stack, x, y, z, this)) { return false } - val metadata = world.getBlockMetadata(x, y, z) world.playAuxSFXAtEntity(this, 2001, x, y, z, blockId + (metadata << 12)) if (stack != null) { diff --git a/li/cil/oc/server/driver/item/FileSystem.scala b/li/cil/oc/server/driver/item/FileSystem.scala index 30d263711..5d0bb85f1 100644 --- a/li/cil/oc/server/driver/item/FileSystem.scala +++ b/li/cil/oc/server/driver/item/FileSystem.scala @@ -48,7 +48,7 @@ object FileSystem extends Item { // if necessary. No one will know, right? Right!? val address = addressFromTag(nbt(item)) Option(oc.api.FileSystem.asManagedEnvironment(oc.api.FileSystem. - fromSaveDirectory(address, capacity, Config.filesBuffered), new ItemLabel(item))) match { + fromSaveDirectory(address, capacity, Config.bufferChanges), new ItemLabel(item))) match { case Some(environment) => environment.node.asInstanceOf[oc.server.network.Node].address = address environment diff --git a/li/cil/oc/server/driver/item/Item.scala b/li/cil/oc/server/driver/item/Item.scala index 0e2809f2a..c95777ca3 100644 --- a/li/cil/oc/server/driver/item/Item.scala +++ b/li/cil/oc/server/driver/item/Item.scala @@ -18,7 +18,7 @@ trait Item extends api.driver.Item { } protected def isOneOf(stack: ItemStack, items: common.item.Delegate*) = - stack.itemID == Items.multi.itemID && (Items.multi.subItem(stack) match { + stack.getItem == Items.multi && (Items.multi.subItem(stack) match { case None => false case Some(subItem) => items.contains(subItem) }) diff --git a/li/cil/oc/server/driver/item/Memory.scala b/li/cil/oc/server/driver/item/Memory.scala index 54c3a2d1f..1ba3576df 100644 --- a/li/cil/oc/server/driver/item/Memory.scala +++ b/li/cil/oc/server/driver/item/Memory.scala @@ -6,7 +6,7 @@ import li.cil.oc.api.driver.Slot import net.minecraft.item.ItemStack object Memory extends Item with driver.Memory { - def amount(item: ItemStack) = if (item.itemID == Items.multi.itemID) Items.multi.subItem(item) match { + def amount(stack: ItemStack) = if (stack.getItem == Items.multi) Items.multi.subItem(stack) match { case Some(memory: li.cil.oc.common.item.Memory) => memory.kiloBytes * 1024 case _ => 0 } else 0 diff --git a/li/cil/oc/util/ExtendedConfiguration.scala b/li/cil/oc/util/ExtendedConfiguration.scala new file mode 100644 index 000000000..fd2680fc6 --- /dev/null +++ b/li/cil/oc/util/ExtendedConfiguration.scala @@ -0,0 +1,43 @@ +package li.cil.oc.util + +import net.minecraftforge.common.Configuration +import scala.language.implicitConversions + +object ExtendedConfiguration { + implicit def extendedConfiguration(config: Configuration) = new ExtendedConfiguration(config) + + class ExtendedConfiguration(config: Configuration) { + def fetch(path: String, default: Boolean, comment: String) = { + val (category, name) = parse(path) + config.get(category, name, default, wrapComment(category, comment)).getBoolean(default) + } + + def fetch(path: String, default: Int, comment: String) = { + val (category, name) = parse(path) + config.get(category, name, default, wrapComment(category, comment)).getInt(default) + } + + def fetch(path: String, default: Double, comment: String) = { + val (category, name) = parse(path) + config.get(category, name, default, wrapComment(category, comment)).getDouble(default) + } + + def fetch(path: String, default: String, comment: String) = { + val (category, name) = parse(path) + config.get(category, name, default, wrapComment(category, comment)).getString + } + + private def parse(path: String) = { + val (category, name) = path.splitAt(path.lastIndexOf(".")) + (category, name.substring(1)) + } + + private def wrapComment(category: String, comment: String) = { + val indent = 1 + category.count(_ == '.') + val wrapRegEx = ("""(.{1,""" + (78 - indent * 4 - 2) + """})(\s|\z)""").r + val cleaned = comment.replace("\r\n", " ").replace("\n", " ").replace("\r", " ").replace("[nl]", "\n").trim() + wrapRegEx.replaceAllIn(cleaned, m => m.group(1).trim() + "\n").stripLineEnd + } + } + +}