Jump to content


  • Posts

  • Joined

  • Last visited

Contact Methods

  • Website URL
    stuck1a.de // kiebel.de

Profile Information

  • Gender
  • Interests

stuck1a's Achievements

  1. Hey there, i've just expirienced some minor issues I've noted, but maybe they are relevant for you. No mods, reproducable. (1) You can open the map from the game menu ("M" / map key). An area marker in West Point flashes in it. (guess its the one for choppers or sth) (2) If you resize the window while booting the game in windowed mode, the UI elements will have a (completely) wrong size as they appear. (3) Shoveling dirt or gravel onto a square creates an IsoObject that stores the original soil as modData.shoveledSprites. This data won't be stored and therefore gets lost when the server restarts. Best regards, stuck1a
  2. Hey there, I hope this is no dead thread. However, I've just noted a few things while coding on my current mod. (1) Static polymorphic dispatch system As example: The timed action queue system. It's a static class which maintains object instances for the current action queue of each character, if any. The point is, if you require any modifications in a derived queue class, you have to add it additionally to the onTick-Event which is of course very performance expensive, because of the static event listener registration "Events.OnTick.Add(ISTimedActionQueue.onTick)" It would be very nice, if there would be a mechanic which would call a derived ISModdedTimedActionQueue.onTick() so you don't have to register additional onTick-Listener. Of course, unregister the vanilla listener is possible, but there are situations as well, in which you still want to use the vanilla listener for the sake of compatibility. At least I just expirienced this situation some days ago (see here). (2) General base item tags Set base tags at least once for all tools/materials, even if it's just Tag="Nails" for something like Nails. Then you wouldn't have to do it mod-side and overloading those items with better cross mod compatibility would be possible. (3) Callbacks for MultiStageBuilding scripts At least a possibility to set an onCreate callback, since currently its completely limited to walls. But there is many potential. For example, you could implement a gradual construction of a larger construction that is only functional from Lvl X (I am thinking of such things as a blast furnace, where stage 0 is the base, stage 1 the combustion chamber, ...) or upgrading structures to unlock new functions/recipes etc. (4) Debugger Exceptions that the debugger throws itself during debugging, e.g. because it doesn't get along with certain Java parts, you could recolor it (e.g. blue instead of red) so that you can see directly when debugging that it can be ignored. Of course, it would be even better if suppressing such exceptions would be possible at all. (5) Parseable ISObject scripts If you outsource ISObjects to parsed scripts and generate them with a factory. (at least the basics of them, like you did for moveables), it would make a lot of things easier. Also, this would make the hard-coded building/welding context menu obsolete. I've just built something similiar (somewhere between base class and factory for ISBuildingObjects) at lua level. The performance is okay, so this shouldn't be a major impact if on JS level. Basically, it would only affect the loading times when joining a server, since everything can get parsed then and added to the hash maps like now. (6) Consumption modifier for items when used as material That would a nice feature in combination with different item types which share a tag. For example, if you add WoodenNails and SteelNails to the normal nails in a mod and cover them all in a recipe via [Recipe.GetItemType.Nails]=10, with WoodenNails having a consumptionModifier=2 and SteelNails a consumptionModifier=0.5 defined, you could cover different quality levels for Nails. Or items using the "Rope" tag as another example: Both, Ropes and SheetRopes are part of it, which makes sense. But since ropes are definitely rarer than the craftable sheet ropes, it'd make sense for balancing, if you can still use sheet ropes, but require twice the amount (or whatever multiplier). It's also logical, since a SheetRope hardly has the same tensile strength as a real rope. Of course, introducing corresponding types/tags such as "WeakRope", "StrongRope" independently would work as well, but that would very quickly lead to a huge number of types/tags and the cross compatibility benefits of the item tags wouldn't apply any more. I hope I can contribute to help you improve your game with that a bit. It's already a nice one and I'm excited to see where the journey will take you and us. Best regards, stuck1a
  3. Ah and btw: You can implement such logic by using the GlobalObject event handlers. Here is an example class of mine where I've added an reuseable global object class to implement different types of water collectors as global objects with the capabilities of the vanilla collectors/barrels. It's a bit more, then you require, but maybe it will still help you to get a better undertanding of the global object system. ISWaterCollector.lua if not ISExtBuildingObject then require 'ExtBuilding/BuildingObjects/ISExtBuildingObject' end --- @class ISWaterCollector : ISExtBuildingObject ISWaterCollector = ISExtBuildingObject:derive('ISWaterCollector') ISWaterCollector.defaults = { hasSpecialTooltip = true, isoData = { isoName = 'watercollector', -- used as fallback name for the iso object systemName = 'watercollector', -- used as name for the map object system objectModDataKeys = { 'waterAmount', 'waterMax', 'addWaterPerAction' }, }, properties = { waterAmount = 0, waterMax = 100, addWaterPerAction = 1 } } ISWaterCollector.registeredRecipes = {} --- --- Java object constructor - initializes and places a completed water collector --- @param x number Target cell X coordinate (goes from north to south) --- @param y number Target cell Y coordinate (goes from west to east) --- @param z number Target cell level (0 = surface, 7 = highest possible layer) --- @param north boolean Whether the north sprite was chosen --- @param sprite string Name of the chosen sprite --- function ISWaterCollector:create(x, y, z, north, sprite) ISExtBuildingObject.create(self, x, y, z, north, sprite) self.javaObject:getModData()['waterMax'] = self.waterMax self.javaObject:getModData()['waterAmount'] = self.waterAmount self.javaObject:getModData()['addWaterPerAction'] = self.addWaterPerAction self.javaObject:transmitCompleteItemToServer() if getCore():getGameMode() ~= 'Multiplayer' then triggerEvent('OnObjectAdded', self.javaObject) end end --- --- Lua object constructor - generates a new water collector object --- @param player number Target player ID --- @param recipe table The building definition - used to add/alter class fields/properties/modData --- @return ISWaterCollector BuildingObject instance --- function ISWaterCollector:new(player, recipe) local o = ISExtBuildingObject.new(self, player, recipe) setmetatable(o, self) self.__index = self return o end --- --- Extension of the ghost tile placement validation --- @param square IsoGridSquare Clicked square object --- @return boolean True, if building can be placed on current target square --- function ISWaterCollector:isValid(square) -- base rules (valid, walkable, free space, reachable, solid ground, etc) if not ISExtBuildingObject.isValid(self, square) then return false end -- only on surface if not getSpecificPlayer(self.player):getZ() == 0 then return false end -- not under stairs if buildUtil.stairIsBlockingPlacement(square, true) then return false end -- tile must have any exterior, natural ground (except water) for i=1, square:getObjects():size() do local props = square:getProperties() if props:Is(IsoFlagType.water) then return false end local obj = square:getObjects():get(i-1) local textureName = obj:getTextureName() or 'occupied' if (not luautils.stringStarts(textureName, 'floors_exterior_natur')) and (not luautils.stringStarts(textureName, 'blends_natur')) then return false end end return true end --- --- Creates an on hover tooltip for water collectors with an amount bar if near enough --- @param tooltipUI UIElement Tooltip factory --- @param square IsoGridSquare Clicked square --- local function DoSpecialTooltip(tooltipUI, square) local oPlayer = getSpecificPlayer(0) if not oPlayer or oPlayer:getZ() ~= square:getZ() or oPlayer:DistToSquared(square:getX() + 0.5, square:getY() + 0.5) > 4 then return end local oIsoObject = CWaterCollectorSystem.instance:getIsoObjectOnSquare(square) if not oIsoObject or not oIsoObject:getModData()['waterMax'] then return end local name = getText(oIsoObject:getTable().displayName or ISWaterCollector.defaults.displayName or ISExtBuildingObject.defaults.displayName) local font = UIFont.Small local fontHeight = getTextManager():getFontFromEnum(font):getLineHeight() tooltipUI:setHeight(6 + fontHeight + 6 + fontHeight + 12) local textX, textY = 12, 6 + fontHeight + 6 local barWid, barHgt = 80, 4 local barX = textX + getTextManager():MeasureStringX(font, getText('IGUI_invpanel_Remaining')) + 12 local barY = textY + (fontHeight - barHgt) / 2 + 2 tooltipUI:setWidth(barX + barWid + 12) tooltipUI:DrawTextureScaledColor(nil, 0, 0, tooltipUI:getWidth(), tooltipUI:getHeight(), 0, 0, 0, 0.75) tooltipUI:DrawTextCentre(getText(name), tooltipUI:getWidth() / 2, 6, 1, 1, 1, 1) tooltipUI:DrawText(getText('IGUI_invpanel_Remaining'), textX, textY, 1, 1, 1, 1) local percent = oIsoObject:getWaterAmount() / oIsoObject:getModData()['waterMax'] if percent < 0 then percent = 0 end if percent > 1 then percent = 1 end local amountWidth = math.floor(barWid * percent) if percent > 0 then amountWidth = math.max(amountWidth, 1) end tooltipUI:DrawTextureScaledColor(nil, barX, barY, amountWidth, barHgt, 0, 0.6, 0, 0.7) tooltipUI:DrawTextureScaledColor(nil, barX + amountWidth, barY, barWid - amountWidth, barHgt, 0.15, 0.15, 0.15, 1) end Events.DoSpecialTooltip.Add(DoSpecialTooltip) if isClient() or getCore():getGameMode() ~= 'Multiplayer' then require 'Map/CGlobalObjectSystem' --- @class CWaterCollectorSystem : CGlobalObjectSystem CWaterCollectorSystem = CGlobalObjectSystem:derive('CWaterCollectorSystem') --- --- Creates a new JS global object system on client-side --- @return CGlobalObjectSystem New controller instance of this global object system type --- function CWaterCollectorSystem:new() return CGlobalObjectSystem.new(self, ISWaterCollector.defaults.isoData.isoName or ISExtBuildingObject.isoData.isoName) end --- --- Checks, if a given IsoObject is a water collector or not (client-side) --- @param isoObject userdata Target buildings JS object --- @return boolean True, if the object is linked to this system --- function CWaterCollectorSystem:isValidIsoObject(isoObject) if instanceof(isoObject, ISWaterCollector.defaults.isoData.isoType or ISExtBuildingObject.defaults.isoData.isoType) and #(ISWaterCollector.registeredRecipes) > 0 then for i=1, #(ISWaterCollector.registeredRecipes) do if ISWaterCollector.registeredRecipes[i] == isoObject:getName() then return true end end end return false end --- --- Creates a new global object controller on client-side --- @param globalObject CGlobalObject Target global object type --- @return CGlobalObject New instance of the target object type --- function CWaterCollectorSystem:newLuaObject(globalObject) return CWaterCollectorGlobalObject:new(self, globalObject) end CGlobalObjectSystem.RegisterSystemClass(CWaterCollectorSystem) require 'Map/CGlobalObject' --- @class CWaterCollectorGlobalObject : CGlobalObject CWaterCollectorGlobalObject = CGlobalObject:derive('CWaterCollectorGlobalObject') --- --- Creates a new global object on client-side --- @param luaSystem CGlobalObjectSystem Global object controller --- @param globalObject CGlobalObject Target global object --- @return CGlobalObject New instance of the target global object --- function CWaterCollectorGlobalObject:new(luaSystem, globalObject) return CGlobalObject.new(self, luaSystem, globalObject) end --- @class ISWaterCollectorMenu ISWaterCollectorMenu = {} --- --- Checks whether any right click hit a watercollector global object and if so, --- adds its context menu items (including debug options in debug mode) --- @param player int ID of the player who did the right click --- @param context ISContextMenu The current context menu object --- @param worldobjects table Global objects found on the clicked point --- @param test boolean Whether this call is a fetch only call for controller support --- function ISWaterCollectorMenu.OnFillWorldObjectContextMenu(player, context, worldobjects, test) if test and ISWorldObjectContextMenu.Test then return true end local found, isoObject = false for _,v in ipairs(worldobjects) do local square = v:getSquare() if square then for i=1, square:getObjects():size() do local v = square:getObjects():get(i-1) if CWaterCollectorSystem.instance:isValidIsoObject(v) then isoObject = v found = true break end end end if found then break end end if not found then return end local oPlayer = getSpecificPlayer(player) if isoObject and isoObject:getSquare():getBuilding() == oPlayer:getBuilding() then -- main option with tooltip local name = getText(isoObject:getTable().displayName or ISWaterCollector.defaults.displayName or ISExtBuildingObject.defaults.displayName) local subMenu = context:getNew(context) local subOption = context:addOptionOnTop(name) context:addSubMenu(subOption, subMenu) local tooltip = ISWorldObjectContextMenu.addToolTip() -- make use of the vanilla tooltip pool tooltip:setName(name) local tx = getTextManager():MeasureStringX(tooltip.font, getText('ContextMenu_WaterName') .. ':') + 20 tooltip.description = string.format('%s: <SETX:%d> %d / %d', getText('ContextMenu_WaterName'), tx, isoObject:getWaterAmount(), isoObject:getWaterMax()) tooltip.maxLineWidth = 512 subOption.toolTip = tooltip -- option "pour on ground" local optionPour = subMenu:addOption(getText('ContextMenu_Pour_on_Ground'), isoObject, ISWaterCollectorMenu.emptyWaterCollector, oPlayer) if not isoObject:hasWater() then optionPour.onSelect = nil optionPour.notAvailable = true end -- option "add water from item" local oInv = oPlayer:getInventory() rainCollectorBarrel = isoObject ISWorldObjectContextMenu.addWaterFromItem(test, context, worldobjects, oPlayer, oInv) local oldOption = context:getOptionFromName(getText('ContextMenu_AddWaterFromItem')) if oldOption ~= nil then -- xcopy the option to the correct index local newOption if context:getOptionFromName('ContextMenu_Drink') ~= nil then newOption = context:insertOptionBefore(getText('ContextMenu_Drink'), oldOption.name, oldOption.target, nil) else newOption = context:insertOptionBefore(getText('ContextMenu_Walk_to'), oldOption.name, oldOption.target, nil) end context:addSubMenu(newOption, context:getSubMenu(oldOption.subOption)) context:removeLastOption() -- the vanilla one was inserted at bottom end -- add debug options if isDebugEnabled() then -- if there are no other object debug options, the menu must be recreated local debugOption = context:getOptionFromName('Objects') if debugOption == nil then if context:getOptionFromName('UIs') then debugOption = context:insertOptionAfter('UIs', 'Objects', worldobjects) debugOption.iconTexture = getTexture('media/ui/BugIcon.png') else debugOption = context:addDebugOption('Objects', worldobjects) end end local debugSubMenu = ISContextMenu:getNew(context) context:addSubMenu( debugOption, debugSubMenu) debugSubMenu:addOption(name .. ': Zero Water', isoObject, ISWaterCollectorMenu.OnWaterCollectorZeroWater, oPlayer) debugSubMenu:addOption(name .. ': Set Water', isoObject, ISWaterCollectorMenu.OnWaterCollectorSetWater) end end end --- --- Removes all the water from the water collector --- @param obj CGlobalObject Target global object instance --- @param oPlayer IsoPlayer Acting player object instance --- function ISWaterCollectorMenu.emptyWaterCollector(obj, oPlayer) if luautils.walkAdj(oPlayer, obj:getSquare()) then ISTimedActionQueue.add(ISEmptyRainBarrelAction:new(oPlayer, obj)) end end --- --- Outsourced part of OnWaterCollectorSetWater - executes the action --- @param _ any Target object (nil) --- @param button ISButton The clicked button --- @param obj CWaterCollectorGlobalObject The global object instance of interest --- local function OnWaterCollectorConfirm(_, button, obj) if button.internal == 'OK' then local playerObj = getSpecificPlayer(0) local text = button.parent.entry:getText() if tonumber(text) then local waterAmt = math.min(tonumber(text), obj:getWaterMax()) waterAmt = math.max(waterAmt, 0.0) local args = { x = obj:getX(), y = obj:getY(), z = obj:getZ(), index = obj:getObjectIndex(), amount = waterAmt } sendClientCommand(playerObj, 'object', 'setWaterAmount', args) end end end --- --- Debug option which opens a UI to adjust the water amount of the water collector. --- Execution after confirmation is outsourced to a local function for performance --- @param obj CWaterCollectorGlobalObject Target global object instance --- function ISWaterCollectorMenu.OnWaterCollectorSetWater(obj) local luaObject = CWaterCollectorSystem.instance:getLuaObjectOnSquare(obj:getSquare()) if not luaObject then return end local modal = ISTextBox:new(0, 0, 280, 180, string.format('Water (0-%d):', obj:getWaterMax()), tostring(obj:getWaterAmount()), nil, OnWaterCollectorConfirm, nil, obj) modal:initialise() modal:addToUIManager() end --- --- Debug option to set the water amount of the water collector to zero --- @param obj CWaterCollectorGlobalObject Target global object instance --- @param oPlayer IsoPlayer Acting player object instance --- function ISWaterCollectorMenu.OnWaterCollectorZeroWater(obj, oPlayer) local args = { x = obj:getX(), y = obj:getY(), z = obj:getZ(), index = obj:getObjectIndex(), amount = 0 } sendClientCommand(oPlayer, 'object', 'setWaterAmount', args) end Events.OnFillWorldObjectContextMenu.Add(ISWaterCollectorMenu.OnFillWorldObjectContextMenu) end if isServer() or getCore():getGameMode() ~= 'Multiplayer' then require 'Map/SGlobalObjectSystem' --- @class SWaterCollectorSystem : SGlobalObjectSystem SWaterCollectorSystem = SGlobalObjectSystem:derive('SWaterCollectorSystem') --- --- Creates a new JS global object system on server-side --- function SWaterCollectorSystem:new() return SGlobalObjectSystem.new(self, ISWaterCollector.defaults.isoData.isoName or ISExtBuilding.isoData.isoName) end --- --- Initialises the controller by defining which fields --- of the building object are relevant for the controller --- function SWaterCollectorSystem:initSystem() SGlobalObjectSystem.initSystem(self) self.system:setModDataKeys(ISWaterCollector.defaults.isoData.modDataKeys or ISExtBuildingObject.defaults.isoData.modDataKeys or {}) self.system:setObjectModDataKeys(ISWaterCollector.defaults.isoData.objectModDataKeys or ISExtBuildingObject.defaults.isoData.objectModDataKeys or {}) self:convertOldModData() end --- --- Creates a new global object controller (server-side) --- @param globalObject SWaterCollectorGlobalObject Target global object type --- function SWaterCollectorSystem:newLuaObject(globalObject) return SWaterCollectorGlobalObject:new(self, globalObject) end --- --- Checks, if a given IsoObject is a water collector or not (server-side) --- @param isoObject userdata Target buildings JS object --- @return boolean True, if the object is linked to this system --- function SWaterCollectorSystem:isValidIsoObject(isoObject) if instanceof(isoObject, ISWaterCollector.defaults.isoData.isoType or ISExtBuildingObject.defaults.isoData.isoType) and #(ISWaterCollector.registeredRecipes) > 0 then for i=1, #(ISWaterCollector.registeredRecipes) do if ISWaterCollector.registeredRecipes[i] == isoObject:getName() then return true end end end return false end -- TODO: Checken, ob es auch ohne die geht, immerhin wird es hierfür keine oldModData geben --- --- For backwards compatibility --- If the gos_xxx.bin file existed, don't touch GameTime modData in case mods are using it --- function SWaterCollectorSystem:convertOldModData() if self.system:loadedWorldVersion() ~= -1 then return end end --- --- Increases the water amount of the buildings JS objects --- function SWaterCollectorSystem:refill() for i=1, self:getLuaObjectCount() do local luaObject = self:getLuaObjectByIndex(i) if luaObject and luaObject.waterAmount < luaObject.waterMax then luaObject.waterAmount = math.min(luaObject.waterMax, luaObject.waterAmount + luaObject.addWaterPerAction) local isoObject = luaObject:getIsoObject() if isoObject then isoObject:setWaterAmount(luaObject.waterAmount) isoObject:transmitModData() end end end end --- --- Listener-Wrapper to invoke the refill method of each water collector instance --- local function EveryTenMinutes() SWaterCollectorSystem.instance:refill() end --- --- Writes the new water amount from global object to this lua object --- @param object IsoObject Target buildings JS object instance --- @param _ int Previous water amount --- local function OnWaterAmountChange(object, _) if not object then return end local luaObject = SWaterCollectorSystem.instance:getLuaObjectAt(object:getX(), object:getY(), object:getZ()) if luaObject then luaObject.waterAmount = object:getWaterAmount() end end SGlobalObjectSystem.RegisterSystemClass(SWaterCollectorSystem) Events.EveryTenMinutes.Add(EveryTenMinutes) Events.OnWaterAmountChange.Add(OnWaterAmountChange) require 'Map/SGlobalObject' --- @class SWaterCollectorGlobalObject : SGlobalObject SWaterCollectorGlobalObject = SGlobalObject:derive('SWaterCollectorGlobalObject') --- --- Creates a new global object (server-side) --- @param luaSystem SGlobalObjectSystem Global object controller --- @param globalObject SGlobalObject Target global object --- function SWaterCollectorGlobalObject:new(luaSystem, globalObject) return SGlobalObject.new(self, luaSystem, globalObject) end --- --- Initialises a new global object --- function SWaterCollectorGlobalObject:initNew() self.waterAmount = ISWaterCollector.defaults.properties.waterAmount self.waterMax = ISWaterCollector.defaults.properties.waterMax self.addWaterPerAction = ISWaterCollector.defaults.properties.addWaterPerAction end --- --- Transfers the current vales from the buildings JS object to the global object --- @param isoObject IsoObject Target buildings JS object instance --- function SWaterCollectorGlobalObject:stateFromIsoObject(isoObject) self.waterAmount = isoObject:getWaterAmount() self.waterMax = isoObject:getModData().waterMax self.addWaterPerAction = isoObject:getModData().addWaterPerAction isoObject:getModData().waterMax = self.waterMax isoObject:getModData().addWaterPerAction = self.addWaterPerAction isoObject:transmitModData() end --- --- Transfers the current values from the global object to the buildings JS object --- @param isoObject IsoObject Target buildings JS object instance --- function SWaterCollectorGlobalObject:stateToIsoObject(isoObject) if not self.waterAmount then self.waterAmount = ISWaterCollector.defaults.properties.waterAmount end if not self.waterMax then self.waterMax = ISWaterCollector.defaults.properties.waterMax end if not self.addWaterPerAction then self.addWaterPerAction = ISWaterCollector.defaults.properties.addWaterPerAction end isoObject:setWaterAmount(self.waterAmount) isoObject:getModData().waterMax = self.waterMax isoObject:getModData().addWaterPerAction = self.addWaterPerAction isoObject:transmitModData() end end --- --- Initialises the global objects of all existing building JS objects while loading the map --- @param isoObject IsoObject Target building object JS object instance --- local function loadGlobalObject(isoObject) if not instanceof(isoObject, ISWaterCollector.defaults.isoData.isoType or ISExtBuildingObject.defaults.isoData.isoType) then return end SWaterCollectorSystem.instance:loadIsoObject(isoObject) end --- --- Checks the recipe definitions for any recipe which uses ISWaterCollector as targetClass --- and adds it to the registry, so we can differ between those recipes in the system classes --- and gather the overloaded sprite and name. --- local function registerWaterCollectors(recipes) for _,v in pairs(recipes) do if v ~= nil then if v.targetClass == nil and type(v) == 'table' then registerWaterCollectors(v) elseif v.targetClass == 'ISWaterCollector' then if isServer() or getCore():getGameMode() ~= 'Multiplayer' then local sprite = v.sprites.sprite or ISWaterCollector.defaults.sprites.sprite or ISExtBuildingObject.defaults.sprites.sprite local priority = v.isoData.mapObjectPriority or ISWaterCollector.defaults.isoData.mapObjectPriority or ISExtBuildingObject.defaults.isoData.mapObjectPriority MapObjects.OnLoadWithSprite(sprite, loadGlobalObject, priority) end table.insert(ISWaterCollector.registeredRecipes, v.isoData.isoName or ISWaterCollector.defaults.isoData.isoName or ISExtBuildingObject.defaults.isoData.isoName) end end end end registerWaterCollectors(ExtBuildingContextMenu.BuildingRecipes) The ISExtBuildingObject is just the base class, where my ISBuildings will get their standard functions/methods and adding tooltips etc. Here's the part where the default values are set: -- Generic defaults (those are absolutely mandatory for initialisation) ISExtBuildingObject.defaults = { displayName = 'Unnamed', buildTime = 200, baseHealth = 200, mainMaterial = 'wood', -- decides which skill lvl determines the extra health (allowed is "wood", "metal", "stone" or "glass") hasSpecialTooltip = false, breakSound = 'BreakObject', craftingBank = 'BuildingGeneric', -- used sound file while performing the build action (it will alternate with tool sounds of the first two tool requirements defined as modData "keep:" entry. It can be used for regular construction sounds as well as "real" crafting bank sounds. sprites = { sprite = 'invisible_01_0' }, isoData = { isoName = 'unnamed', -- defines the name of the global map object instance, if any. If a global object has several subtypes (like in "watercollector"), this might be used to differ between those subtypes (like "waterwell", "rainbarrel"). If there are no subtypes, then it can simply use the same value as its systemName (name of the associated global object system, which must be unique) isoType = 'IsoThumpable', mapObjectPriority = 7 }, modData = {} } And one useage would the my water well implementation, which defines a concrete WaterCollector global object: if not ExtBuildingContextMenu then ExtBuildingContextMenu = {} end if not ExtBuildingContextMenu.call then ExtBuildingContextMenu.call = function(callback, ...) return callback(...) end setmetatable(ExtBuildingContextMenu, {__call = ExtBuildingContextMenu.call}) end --- --- This table will be used in ExtBuilding_ContextMenu.lua --- to create all submenus and their recipes of the new build menu --- ExtBuildingContextMenu.BuildingRecipes = { ContextMenu_ExtBuilding_Cat__Technology = { { targetClass = 'ISWaterCollector', displayName = 'ContextMenu_ExtBuilding_Obj__WaterWell', tooltipDesc = 'Tooltip_ExtBuilding__WaterWell', buildTime = 700, baseHealth = 600, mainMaterial = 'stone', completionSound = 'BuildFenceCairn', isoData = { isoName = 'waterwell' }, properties = { waterAmount = 50, waterMax = 5000, addWaterPerAction = 5, craftingBank = 'Shoveling' }, sprites = { sprite = 'garteneden_tech_01_0', northSprite = 'garteneden_tech_01_1' }, modData = { ['keep:' .. utils.concatItemTypes({'Hammer'})] = 'Base.Hammer', ['keep:' .. utils.concatItemTypes({'Saw'})] = 'Base.Saw', ['keep:' .. utils.concatItemTypes({'DigGrave'})] = 'Base.Shovel', ['need:Base.Rope'] = 5, ['need:Base.Plank'] = 4, ['need:Base.Nails'] = 10, ['need:Base.Stone'] = 20, ['use:Base.Gravelbag'] = 8, ['need:Base.BucketEmpty'] = 1, ['requires:Woodwork'] = 7, ['requires:Fitness'] = 5, ['xp:Woodwork'] = 5, ['xp:Fitness'] = 5 }, }, }, }
  4. That's a cool idea! May I adapt it for my current server project? Of course I'll post the code here when I'm ready. (would take a while as I'm still working on the base classes for a build system makeover)
  5. No, but the IsoLivingCharacters classes are very flexible. You should be able to simply replace the Bob models with those for zombies and then implement all the behaviours you said. Of course, this would be a major makeover which will require a lot of time. Guess it would make sense to mix up NPC functions with those of the zed functions to build that "more or less" controllable horde. Well, it's all about balancing. It'd require a lot of rebalancing for sure, but hey ho, why not. That's what makes a mod a good mod, isn't it?
  6. Hey there, I just wanted to share some of my util scripts I recently use with IDEA run configurations. Maybe anyone else will benefit from it, too. The batch scripts will of course only work under windows. UpdateSteamWorkshopObject.bat @ECHO off :: Update-Script for Zomboid Steam-WorkshopItems :: Usage-Example: :: UpdateSteamWorkshopObject 522891356 :: Adjust these paths as needed SET steamcmd=E:/Programme/SteamCMD/steamcmd.exe SET steamdir=E:/Programme/Steam "%steamcmd%" +force_install_dir "%steamdir%" +login anonymous +workshop_download_item 108600 %~1 +quit HardresetDebugServer.bat :: Path to your dev servers "zomboid" directory :: Use with care, it will delete all multiplayer save files within in and the server database file SET zomboid_directory=E:\Development\Java Workspace\ProjectZomboidModding\appdata SET servername=DebugServer DEL "%zomboid_directory%\db\%servername%.db" /F /Q DEL "%zomboid_directory%\Saves\Multiplayer\" /S /F /Q FOR /D %%a IN ("%zomboid_directory%\Saves\Multiplayer\*.*") DO RD /Q /S "%%a" EXIT spawnpoints2objects.lua -- Script to convert spawnpoints in the format like given in spawnpoints.lua files to the format required for objects.lua in maps. -- I mainly use this to fix/adjust spawnpoints of 3rd party maps I adapt. -- EXAMPLE DATA. Replace as needed spawnpoints = { unemployed = { { worldX = 38, worldY = 23, posX = 80, posY = 124, posZ = 0 }, { worldX = 38, worldY = 22, posX = 243, posY = 96, posZ = 0 }, { worldX = 38, worldY = 22, posX = 94, posY = 102, posZ = 0 }, { worldX = 38, worldY = 22, posX = 12, posY = 124, posZ = 0 }, { worldX = 37, worldY = 22, posX = 197, posY = 130, posZ = 0 }, }, cook = { { worldX = 38, worldY = 23, posX = 122, posY = 12, posZ = 1 }, { worldX = 38, worldY = 22, posX = 12, posY = 287, posZ = 0 }, }, } for profession, entries in pairs(spawnpoints) do local prof = tostring(profession) if prof == 'unemployed' then prof = 'all' end for _, v in pairs(entries) do print("{ name = '', type = 'SpawnPoint', x = "..v.worldX*300+v.posX..", y = "..v.worldY*300+v.posY..", z = "..(v.posZ or "0")..", width = 1, height = 1, properties = { Professions = '"..prof.."' } },") end end -- THIS WILL OUTPUT: -- { name = '', type = 'SpawnPoint', x = 11522, y = 6912, z = 1, width = 1, height = 1, properties = { Professions = 'cook' } }, -- { name = '', type = 'SpawnPoint', x = 11412, y = 6887, z = 0, width = 1, height = 1, properties = { Professions = 'cook' } }, -- { name = '', type = 'SpawnPoint', x = 11480, y = 7024, z = 0, width = 1, height = 1, properties = { Professions = 'all' } }, -- { name = '', type = 'SpawnPoint', x = 11643, y = 6696, z = 0, width = 1, height = 1, properties = { Professions = 'all' } }, -- { name = '', type = 'SpawnPoint', x = 11494, y = 6702, z = 0, width = 1, height = 1, properties = { Professions = 'all' } }, -- { name = '', type = 'SpawnPoint', x = 11412, y = 6724, z = 0, width = 1, height = 1, properties = { Professions = 'all' } }, -- { name = '', type = 'SpawnPoint', x = 11297, y = 6730, z = 0, width = 1, height = 1, properties = { Professions = 'all' } }, GetMapSizeFromWorldmapXML.php #!/usr/bin/php <?php // // This scripts excepts a path to a worldmap.xml file and calculate the map boundaries out of its content. // You can add optional padding. By default, I've set it to 10 extra squares on each edge. // Might be used for auto-generation of MapDefinitions as well (simply parse or adjust the output string in this case) // // EXAMPLE OUTPUT FOR THE WORLDMAP.XML OF BEDFORD FALLS: // XML cell analysis done! // (xmin, ymin, xmax, ymax) = 12890, 9890, 14410, 11410 /********* OPTIONS ***********/ const TILES_PER_CELL = 300; const BOUND_PADDING = 10; /*****************************/ $xmin = null; $ymin = null; $xmax = null; $ymax = null; $cells = []; if ( !$argv || !$argv[1] || $argv[1] == '' ) { echo 'Missing argument'; exit(1); } if ( !file_exists($argv[1]) ) { echo "File {$argv[1]} not found"; exit(1); } $xml = file_get_contents($argv[1]); $xml = simplexml_load_string($xml, 'SimpleXMLElement', LIBXML_NOCDATA); foreach ( $xml as $elem ) { if ( $elem->getName() == 'cell' ) { $cells[] = $elem; } } foreach ( $cells as $cell ) { foreach ( $cell->attributes() as $name => $value ) { if ( $name == 'x' ) { $current = (int)$value * TILES_PER_CELL; foreach ( $cell->children() as $feature ) { if ( $feature->getName() == 'feature' ) { foreach ( $feature->children() as $geometry ) { if ( $geometry->getName() == 'geometry' ) { foreach ( $geometry->children() as $coordinates ) { if ( $coordinates->getName() == 'coordinates' ) { foreach ( $coordinates->children() as $point ) { if ( $point->getName() == 'point' ) { foreach ( $point->attributes() as $key => $val ) { if ( $key == 'x' ) { if ( $xmax == null || $xmax < $current + $val ) { $xmax = $current + $val; } if ( $xmin == null || $xmin > $current + $val ) { $xmin = $current + $val; } } } } } } } } } } } } elseif ( $name == 'y' ) { $current = (int)$value * TILES_PER_CELL; foreach ( $cell->children() as $feature ) { if ( $feature->getName() == 'feature' ) { foreach ( $feature->children() as $geometry ) { if ( $geometry->getName() == 'geometry' ) { foreach ( $geometry->children() as $coordinates ) { if ( $coordinates->getName() == 'coordinates' ) { foreach ( $coordinates->children() as $point ) { if ( $point->getName() == 'point' ) { foreach ( $point->attributes() as $key => $val ) { if ( $key == 'y' ) { if ( $ymax == null || $ymax < $current + $val ) { $ymax = $current + $val; } if ( $ymin == null || $ymin > $current + $val ) { $ymin = $current + $val; } } } } } } } } } } } } } } $xmin -= BOUND_PADDING; $ymin -= BOUND_PADDING; $xmax += BOUND_PADDING; $ymax += BOUND_PADDING; echo "\n\nXML cell analysis done!\n"; echo "(xmin, ymin, xmax, ymax) = ${xmin}, ${ymin}, ${xmax}, ${ymax}\n"; exit(0); ?> If you add the this PHP script as remote tool, you can simply right click on any valid worldmap.xml and call the script. Works similar like the in-game map definitions tool. Here is my remote tool definition as example: Well, I guess that's enough so far. Best regards, stuck1a
  7. Here is basic example snippet: local function MyItemsContextMenuEntry(player, context, items) local items = ISInventoryPane.getActualItems(items) for _, item in ipairs(items) do if item:getFullType() == 'YourModule.YourItemType' then context:addOption(getText('IGUI_YourEntryTranslationString'), getSpecificPlayer(player), YourCallbackFunctionWhenClickedTheMenuEntry) end end end Events.OnFillInventoryObjectContextMenu.Add(MyItemsContextMenuEntry) As best practice, cancel the function as soon as possible, if its clear, that its not targeting any of your desired item(s), because it will be executed every time, you right click on any invetory object. That's why those handlers are mostly wrapped together in the vanilla code or well coded mods. If the event fires, it will send you int player, ISContextMenu context an ArrayList items out of the JS sources, so these three params are defined. You should be able to add optional arguments, if required by adding them as additional arguments of the .Add() call.
  8. Then there is a memory leak somewhere. Either you play with a buggy mod or a corrupted savegame, I guess
  9. As far as i know its deprecated, yes.
  10. There are different types of IsoObjects. For example, there are Tiles. Tiles can be a flooring, which will be rendered at the very back of course. Then there are Special tile objects. They will overlap Floorings. The can be stacked, and will be rendered in exactly that order (newest to oldest) Then there are SpecialObjects which are most commonly solid structures or moveables. The will be rendered over tiles, of course. Then there are many special types, defined by their properties like wallType etc. Basically, they are also special objects, but due to their very own render functions, they will be rendered however it is implemented. (see ISWoodenWall.lua or ISWoodenStairs.lua as example) For know, I did not see any z-Layer implementation exposed to Lua, so I guess you will have to change the objects manually. Something like changing their pointers and force re-render would be the cheapest way, I guess. The worst would be to remove the objects and recreate them in the correct order. Should still be performant as long as it only happens occasionally (e.g. when a player has finished building something)
  11. (Without viewing the described code) In major, such source code is caused by one of the two following reasons: - These functions could/might/will be called from somewhere else out of the "self scope". So if its unclear whether you can just offer a player integer value or the player object, it makes sense to exceöt the int value for such parameters, so you can use this function in a wider range of contexts. A case distinction might be possible as well, of course, but getSpecificPlayer(int) seems to be faster. - The function is a so called "grown structure", which could be optimized due to such changes, but will not, to offer backwards compatibility for older mods/versions.
  12. Hey there. I'm just struggling a bit with my "construction site tile". I use tryBuild() of a derviced ISBaseObject to place it as special tile object at the very beginning of the build process. Of course, it shall disappear whenever any action of the action queue becomes interrupted however. Unfortunately, I could not find a way to transfer appropriate callbacks, so I've decived to derive all TimedAction which might occur and add an additional call for the tile removal check in their stop() functions. Not elegant, but workes. Bad luck - the faceDirection function is no TA on its own, so I've tried to hook into TimedActionQueue:tick() with no luck because of the static nature and lack of polymorphic. So for now I've set an event listener to work around the problem by calling a derived variant of queue:tick(). It works, but as you can imagine, I'm not very happy with this solution because of the high performance impact such a listener comes with. Surely I just overlooked the obvious solution, I guess a more elegant solution can be implemented with waitToStart() or the ISQueueActionsAction class, but I really can't figure it out right now. If somehow possible, I'd like to avoid replacing the whole vanilla queue class for best possible compatibility with future updates. Here the merged code for a better understanding of what I'm trying to achieve: --- Removes the construction site tile object linked to the active action item --- @param isoTile IsoObject construction site tile pointer local function removeConstructionSite(isoTile) local square = isoTile:getSquare() -- there might be several construction sites on the square, so remove only the assigned one local specialTiles = square:getSpecialObjects() for i=0, specialTiles:size()-1 do if specialTiles:get(i) == isoTile then square:transmitRemoveItemFromSquare(isoTile) square:RemoveTileObject(isoTile) isoTile = nil return end end end require 'TimedActions/ISBaseTimedAction' ---@class ISExtBuildAction : ISBuildAction ISExtBuildAction = ISBuildAction:derive('ISExtBuildAction') function ISExtBuildAction:new(character, item, x, y, z, north, spriteName, time, tool1, tool2, isoTile) local o = ISBuildAction.new(self, character, item, x, y, z, north, spriteName, time) setmetatable(o, self) self.__index = self -- [...] (shortened) o.isoTile = isoTile return o end function ISExtBuildAction:perform() -- [...] (shortened) if self.isoTile ~= nil then removeConstructionSite(self.isoTile) end ISBuildAction.perform(self) end function ISExtBuildAction:stop() ISBuildAction.stop(self) if self.isoTile ~= nil then removeConstructionSite(self.isoTile) end end -- DERVICED VARIANTS OF ALL POSSIBLY USED TA's ---@class ISExtInventoryTransferAction : ISInventoryTransferAction ISExtInventoryTransferAction = ISInventoryTransferAction:derive('ISExtInventoryTransferAction') function ISExtInventoryTransferAction:new(character, item, srcContainer, destContainer, isoTile) local o = ISInventoryTransferAction:new(character, item, srcContainer, destContainer) setmetatable(o, self) self.__index = self o.isoTile = isoTile return o end function ISExtInventoryTransferAction:stop() ISInventoryTransferAction.stop(self) if self.isoTile ~= nil then removeConstructionSite(self.isoTile) end end -- and so on.... -- NOT VERY NICE, BUT WORKS FOR NOW... ---@class ISExtTimedActionQueue : ISTimedActionQueue ISExtTimedActionQueue = ISTimedActionQueue:derive('ISExtTimedActionQueue') function ISExtTimedActionQueue:clearQueue() ISTimedActionQueue.clearQueue(self) end function ISExtTimedActionQueue:resetQueue() ISTimedActionQueue.resetQueue(self) end function ISExtTimedActionQueue:new(character) local o = ISTimedActionQueue:new(character) setmetatable(o, self) self.__index = self return o end function ISExtTimedActionQueue.getTimedActionQueue(character) local queue = ISExtTimedActionQueue.queues[character] if queue == nil then queue = ISExtTimedActionQueue:new(character) end return queue end function ISExtTimedActionQueue.add(action) if action.ignoreAction then return end if instanceof(action.character, 'IsoGameCharacter') and action.character:isAsleep() then return end local queue = ISExtTimedActionQueue.getTimedActionQueue(action.character) local current = queue.queue[1] if current and (current.Type == 'ISQueueActionsAction') and current.isAddingActions then table.insert(queue.queue, current.indexToAdd, action) current.indexToAdd = current.indexToAdd + 1 return queue end queue:addToQueue(action) return queue end function ISExtTimedActionQueue.queueActions(character, addActionsFunction, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10) local action = ISQueueActionsAction:new(character, addActionsFunction, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10) return ISExtTimedActionQueue.add(action) end function ISExtTimedActionQueue.hasAction(action) if action == nil then return false end local queue = ISExtTimedActionQueue.queues[action.character] if queue == nil then return false end return queue:indexOf(action) ~= -1 end function ISExtTimedActionQueue.clear(character) character:StopAllActionQueue() local queue = ISExtTimedActionQueue.getTimedActionQueue(character) queue:clearQueue() return queue end function ISExtTimedActionQueue:tick() local action = self.queue[1] if action == nil then self:clearQueue() return end if not action.character:getCharacterActions():contains(action.action) then if action.isoTile ~= nil then removeConstructionSite(action.isoTile) end self:resetQueue() return end end function ISExtTimedActionQueue.onTick() for _,queue in pairs(ISExtTimedActionQueue.queues) do queue:tick() end end Events.OnRenderTick.Add(ISExtTimedActionQueue.onTick)
  13. Scripts must be parsed by the ScriptManager, to get this done at runtime, you must reinit the mod which contains them. Here are som example calls: getCore():ResetLua('default') -- reinit everythings getCore():ResetLua('foo') -- reinit files from mod with id "foo" getCore():ResetLua('foo', _G['bar.baz']) -- reinit mod "foo" and call global object bar.baz I never came up to test the third one since I didn't need that yet - just found it in the js classes and noted it for now. Might be useful for live mod updates or whatever. But for debugging the first and second should be enough.
  14. Hey there, even if you have already solved your problem, just let me answer for anyone else who will encounter this problem in future as wel.. There is actual a possibility to remove existing context menu items. You just need the correspnding context object. I'm currently working on finishing an old, unfinished mod from blindc0der. Since it is a build overhaul, I had to remove the vanilla "Build" and "Metal Wielding" objects. You can either overload the build script or do it as follows: BCCrafTec.WorldMenu = function(player, context, worldObjects) -- Remove vanilla menus context:removeOptionByName(getText("ContextMenu_Build")); -- context is an instance of ISContextMenu context:removeOptionByName(getText("ContextMenu_MetalWelding")); -- Add CrafTec build menu -- ...unrelated... end Events.OnFillWorldObjectContextMenu.Add(BCCrafTec.WorldMenu); -- The event will give you the required context Hope this will help someone else in future Best regards stuck1a
  15. If your mod contains a lot of items which shall be distributed, you might use this snippet as well for a single ItemDistributions.lua per mod: local ItemDist = { -- LootableMaps { Distributions = { {"CrateMaps", 50}, {"CrateMaps", 20}, {"CrateMechanics", 2}, {"GasStorageMechanics", 2}, {"MagazineRackMaps", 20}, {"MagazineRackMaps", 10}, {"StoreShelfMechanics", 2}, {"StoreShelfMechanics", 2}, }, Items = { "CoryerdonMap", "RavenCreekMap", "KingsmouthMap", "BlackwoodMap", "FirecampMap", } }, -- Technical Magazines { Distributions = { {"BookstoreMisc", 2}, {"CrateMagazines", 1}, {"LibraryBooks", 1}, {"LivingRoomShelf", 0.1}, {"LivingRoomShelfNoTapes", 0.1}, {"LivingRoomSideTable", 0.1}, {"LivingRoomSideTableNoRemote", 0.1}, {"MagazineRackMixed", 1}, {"PostOfficeMagazines", 1}, {"ShelfGeneric", 0.1}, {"ToolStoreBooks", 1}, }, Items = { "WoodenLadderMagazine", "IronLadderMagazine", } }, -- next item distribution group -- ... } local function getLootTable(strLootTableName) return ProceduralDistributions.list[strLootTableName] end local function insertItem(tLootTable, strItem, iWeight) table.insert(tLootTable.items, strItem) table.insert(tLootTable.items, iWeight) end local function preDistributionMerge() for i=1, #ItemDist do for j=1, #(ItemDist[i].Distributions) do for k=1, #(ItemDist[i].Items) do local tLootTable = getLootTable(ItemDist[i].Distributions[j][1]) local strItem = ItemDist[i].Items[k] local iWeight = ItemDist[i].Distributions[j][2] insertItem(tLootTable, strItem, iWeight) end end end end Events.OnPreDistributionMerge.Add(preDistributionMerge) Junk isn't accounted for yet, but can easily be added
  • Create New...