Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add graphics support for building-hacks #4380

Open
wants to merge 28 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
534c3d8
Add graphics support for building-hacks
warmist Mar 17, 2024
f6d4488
Update to new structures and simplify api a bit
warmist Mar 18, 2024
939f038
Fix bugs and refactor lua c api to use indexed access
warmist Mar 18, 2024
b69fb11
Update plugin lua module
warmist Mar 18, 2024
3800286
whitespace fixes
warmist Mar 19, 2024
1983589
Fix uint32_t getting set to -1
warmist Mar 19, 2024
48c69be
Add event to vmethod interpose setTriggerState for lever/trap plate l…
warmist Mar 20, 2024
a59a3d3
big refactor and api cleanup
warmist Mar 24, 2024
d7da17a
Doc update
warmist Mar 24, 2024
39a9489
Fix bad string compare
warmist Mar 24, 2024
76b0799
whitespace fixes
warmist Mar 24, 2024
8c45267
Add error on failing to find gears for auto-machines and auto-animate
warmist Mar 25, 2024
dbc9f54
Add a way to change graphics tiles when auto generating connection po…
warmist Mar 29, 2024
91485c0
Fix repeated calling animation creating more and more frames
warmist Mar 29, 2024
e5f7c5b
Fix graphics bug
warmist Mar 29, 2024
3d65ad1
Merge branch 'develop' into building_hacks_graphics
myk002 Apr 1, 2024
01402b2
Update docs/dev/Lua API.rst
warmist Apr 1, 2024
72c5c63
Switch map to unordered map
warmist Apr 2, 2024
079bbd1
Just use 0 as invalid tile marker. It's used in df code like that
warmist Apr 2, 2024
0c8e3ce
Redo the categorize to prevent issue that scripts run after world has…
warmist Apr 2, 2024
4935331
Redo plugin to auto-enable on first use and expose enabled state to d…
warmist Apr 2, 2024
cf2f18b
Fix graphics tile types
warmist Apr 2, 2024
13e49e9
docs: add correct tags to the plugin docs
warmist Apr 2, 2024
5479ed1
docs: add note about workshop ids
warmist Apr 5, 2024
5febcdc
remove boolean arg from fixImpassible and setOwnableBuilding and doc …
warmist Apr 5, 2024
22afaa7
Allow passing numeric id to auto functions. Tidy up docs api some more.
warmist Apr 5, 2024
9c9e81d
docs: more info about animation tiles
warmist Apr 5, 2024
313469b
fix lua syntax error and whitespaces
warmist Apr 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
152 changes: 102 additions & 50 deletions docs/dev/Lua API.rst
Original file line number Diff line number Diff line change
Expand Up @@ -5766,82 +5766,134 @@ The names of the functions are also available as the keys of the
building-hacks
==============

This plugin overwrites some methods in workshop df class so that mechanical workshops are possible. Although
plugin export a function it's recommended to use lua decorated function.
This plugin extends DF workshops to support custom powered buildings.

.. note:: when using numeric ids for workshops be aware that those id can change between worlds
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.. note:: when using numeric ids for workshops be aware that those id can change between worlds
.. note::
When using numeric ids for workshop types, be aware that those ids can change
between worlds, depending on what other custom types exist in the raws for that
world.


.. contents::
:local:

Functions
---------

``registerBuilding(table)`` where table must contain name, as a workshop raw name, the rest are optional:
* ``setOwnableBuilding(workshop_type)``

Set workshop to be included in zones (such as bedroom or inn).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would you choose to include the workshop in zones? What does the choice affect?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It allows for interesting systems: one example i had (before big gui changes etc...) was a personal dwarven hobby workshop that would be built in owned rooms. That way you could query what dwarf owns what buildings (e.g. cabinet, bet, modded-hobby-workshop) and issue jobs that would consume resources, give nothing back, but still level skills. Intended to work as a "do this while dwarf is idle".

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it be a good idea to always allow the building to be ownable? If in zone, and zone owned, then building owned?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Set workshop to be included in zones (such as bedroom or inn).
Set workshop to be included in zones (such as a bedroom or tavern).


:workshop_type: custom workshop string id e.g. ``SOAPMAKER`` or numeric id
warmist marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
:workshop_type: custom workshop string id e.g. ``SOAPMAKER`` or numeric id
:workshop_type: custom workshop string id, e.g. ``SOAPMAKER`` or numeric id


* ``fixImpassible(workshop_type)``

Set workshop non walkable tiles to also block liquids (i.e. water and magma).

:workshop_type: custom workshop string id e.g. ``SOAPMAKER`` or numeric id
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
:workshop_type: custom workshop string id e.g. ``SOAPMAKER`` or numeric id
:workshop_type: custom workshop string id, e.g. ``SOAPMAKER`` or numeric id


* ``setMachineInfo(workshop_type, needs_power, power_consumed, power_produced, connection_points)``
warmist marked this conversation as resolved.
Show resolved Hide resolved

Setup and enable machine-like functionality for the workshop. All workshops of this type will have
this as default power consumption/production. Note: due to implementation limitations
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
this as default power consumption/production. Note: due to implementation limitations
this as their default power consumption/production. Note: due to implementation limitations,

workshop only connects to other machines if the other machine is build later than this one.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
workshop only connects to other machines if the other machine is build later than this one.
workshops only connect to other machines if the other machines are built after this one.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

must they be built after the custom workshop, or planned? is it ok if the other machine is planned before the custom workshop is planned, but then the workshop is built and finally the machine is built?


:workshop_type: custom workshop string id e.g. ``SOAPMAKER`` or numeric id
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
:workshop_type: custom workshop string id e.g. ``SOAPMAKER`` or numeric id
:workshop_type: custom workshop string id, e.g. ``SOAPMAKER`` or numeric id

:needs_power: only function if it has sufficient power
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
:needs_power: only function if it has sufficient power
:needs_power: true if the workshop should only be usable if it has sufficient power

:power_consumed: building consumes this amount of power
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
:power_consumed: building consumes this amount of power
:power_consumed: buildings of this type consume this amount of power by default

:power_produced: output this amount of power
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
:power_produced: output this amount of power
:power_produced: buildings of this type output this amount of power by default

:connection_points: a table of ``{x=?,y=?}`` zero-based coordinates that can connect to other machines

* ``setMachineInfoAuto(workshop_type, needs_power, power_consumed, power_produced, [gear_tiles])``

Same as ``setMachineInfo`` but fills out the ``connection_points`` table from raws placing connection
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Same as ``setMachineInfo`` but fills out the ``connection_points`` table from raws placing connection
Same as ``setMachineInfo`` but fills out the ``connection_points`` table based on the building definition in the raws. It places connection

points on tiles which have the gear tile. ``gear_tiles`` is an optional array of two tiles that are
myk002 marked this conversation as resolved.
Show resolved Hide resolved
counted as gears in the workshop ascii tile raws. The default gear tiles are ``42`` and ``15``.

* ``setAnimationInfo(workshop_type, frames, frame_skip)``
warmist marked this conversation as resolved.
Show resolved Hide resolved

Animate workshop by replacing displayed tiles (or graphical tiles). There are two ways this works:
if ``frame_skip>=0`` then it shows each frame for ``frame_skip`` of frames or if ``frame_skip<0``
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what does it mean if frame_skip == 0? should this perhaps be synchronized to other machines if frame_skip <= 0?

Frames are synchronized to the machine this building is connected to.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what if it is connected to multiple machines?


:workshop_type: custom workshop string id e.g. ``SOAPMAKER`` or numeric id
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
:workshop_type: custom workshop string id e.g. ``SOAPMAKER`` or numeric id
:workshop_type: custom workshop string id, e.g. ``SOAPMAKER`` or numeric id

:frames: table of frames. Each frame is sparse flat table with ids from ``0`` to ``31*31-1``.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"sparse flat table" is ambiguous here. all other usage in these docs use "table" to mean "list". here, do you mean a sparse map with numeric keys?

if an element is nil, does that just mean that nothing is rendered there?

finally, is a 0-960 map of elements the best interface for a modder? would it be more natural to express frames as a list of two dimensional sparse maps? e.g. tile metadata can be addressed as safe_index(frames, frame_num, row, col).

regarding the tile metadata, I suggest using pens instead of a sequence of numbers. E.g.

ensure_key(frames, frame_num, row)[col] = dfhack.pen.parse{
    tile=dfhack.screen.findGraphicsTile('WARMIST_DRAGON_ENGINE', col, row),
    ch=string.byte('['),
    fg=COLOR_CYAN,
}

How do graphics tile, overlay tile, signpost tile, and item tile interact? What does it mean if a tile has both a graphics tile and a signpost tile? when would a building define something for the item tile, overriding whatever item is actually on that tile?

Each frame tile is table of integers from 4 to 8 members long. Tile members are as
follow: ``tile``, ``foreground color``, ``background color``, ``bright``,
``graphics tile``, ``overlay tile``, ``signpost tile``, ``item tile``.
First 4 are function same as ascii workshop definition. The latter 4 are graphics
layers. ``signpost tile`` is an optional row that sticks up over the workshop.

:name:
custom workshop id e.g. ``SOAPMAKER``
:frame_skip: How many ticks to display one frame. If set to negative number (or skipped) frames
are synchronized with machine animation.

.. note:: this is the only mandatory field.
* ``setAnimationInfoAuto(workshop_type, make_graphics_too, [frame_length], [gear_tiles])``
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* ``setAnimationInfoAuto(workshop_type, make_graphics_too, [frame_length], [gear_tiles])``
* ``setAnimationInfoAuto(workshop_type, make_graphics_too[, frame_length[, gear_tiles]])``


:fix_impassible:
if true make impassable tiles impassable to liquids too
:consume:
how much machine power is needed to work.
Disables reactions if not supplied enough and ``needs_power==1``
:produce:
how much machine power is produced.
:needs_power:
if produced in network < consumed stop working, default true
:gears:
a table or ``{x=?,y=?}`` of connection points for machines.
:action:
a table of number (how much ticks to skip) and a function which
gets called on shop update
:animate:
a table of frames which can be a table of:
Animate workshop as with function above but generate frames automatically. This works by finding
tiles which have gears and animating them with alternating gear tiles.

a. tables of 4 numbers ``{tile,fore,back,bright}`` OR
b. empty table (tile not modified) OR
c. ``{x=<number> y=<number> + 4 numbers like in first case}``,
this generates full frame useful for animations that change little (1-2 tiles)
:workshop_type: custom workshop string id e.g. ``SOAPMAKER`` or numeric id
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
:workshop_type: custom workshop string id e.g. ``SOAPMAKER`` or numeric id
:workshop_type: custom workshop string id, e.g. ``SOAPMAKER`` or numeric id

:make_graphics_too: replace same tiles in graphics mode with tiles from vanilla df mechanism
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when would you want this set to false?

:frame_length: How many ticks to display one frame. If set to negative number (or skipped) frames
are synchronized with machine animation.
:gear_tiles: Optional array of 2 or 4 indexes. First two define ascii tiles and next two graphics tiles.
This overrides default gear tiles.
Comment on lines +5835 to +5836
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a map might be more instructive instead of a variable length list, e.g.:

local bhacks = require('plugins.building-hacks')
bhacks.setAnimationInfoAuto('DRAGON_ENGINE', true, 4, {
    gear_ch=42, -- looks for this in ascii
    gear_ch_alt=15,  -- replaces with this for animated frames
    gear_tile=dfhack.screen.findGraphicsTile('WARMIST_DRAGON_ENGINE', 1, 0),
    gear_tile_alt=dfhack.screen.findGraphicsTile('WARMIST_DRAGON_ENGINE', 4, 0),
})`


:canBeRoomSubset:
a flag if this building can be counted in room. 1 means it can, 0 means it can't and -1 default building behaviour
:auto_gears:
a flag that automatically fills up gears and animations. It looks over the building definition for gear icons and maps them.
* ``setOnUpdate(workshop_type,interval,callback)``
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* ``setOnUpdate(workshop_type,interval,callback)``
* ``setOnUpdate(workshop_type, interval, callback)``


Animate table also might contain:
Setup callback to be called every ``interval`` of ticks for each building of this type. Note: low interval
numbers and/or many workshops that use this might reduce DF performance.
Comment on lines +5840 to +5841
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Setup callback to be called every ``interval`` of ticks for each building of this type. Note: low interval
numbers and/or many workshops that use this might reduce DF performance.
Register callback to be called every ``interval`` ticks for each building of this
type. This can be very expensive if the interval is low and/or there are many
workshops of this type. Keep these callbacks light!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you may want to explain the difference between frame ticks and world simulation ticks (and note which one this refers to).


:frameLength:
how many ticks does one frame take OR
:isMechanical:
a bool that says to try to match to mechanical system (i.e. how gears are turning)
:workshop_type: custom workshop string id e.g. ``SOAPMAKER`` or numeric id
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
:workshop_type: custom workshop string id e.g. ``SOAPMAKER`` or numeric id
:workshop_type: custom workshop string id, e.g. ``SOAPMAKER`` or numeric id

:interval: how many ticks to skip between event triggers
:callback: function to call. Function signature is ``func(workshop)`` where ``workshop`` is of type
``df.building_workshopst``

``getPower(building)`` returns two number - produced and consumed power if building can be modified and returns nothing otherwise
* ``getPower(building)``

``setPower(building,produced,consumed)`` sets current power production and consumption for a building.
Returns two number - produced and consumed power if building can be modified and returns nothing otherwise.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Returns two number - produced and consumed power if building can be modified and returns nothing otherwise.
If this building is of a type registered with building-hacks, returns values for
produced and consumed power. Otherwise, returns ``nil``.


:building: specific workshop that produces or consumes power

* ``setPower(building, power_consumed, power_produced)``

Sets current power production and consumption for a specific workshop building. Can be used to make buildings that
dynamically change power consumption and production.
Comment on lines +5856 to +5857
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Sets current power production and consumption for a specific workshop building. Can be used to make buildings that
dynamically change power consumption and production.
Dynamically sets current power production and consumption for a specific workshop
(which must be of a type registered with building-hacks).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I note that the order of consumed and produced power is in the opposite order compared to setPower. They should match.


:building: specific workshop that produces or consumes power
:power_consumed: set building to consume this amount of power
:power_produced: output this amount of power


Events
------

This module exports two events. However only one is documented here and is intended to be used directly. To use
``onUpdateAction`` instead call ``setOnUpdate`` function.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why does onUpdateAction exist as a Lua event, then?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The plugin lua part still uses that event (i.e. setOnUpdate function)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not convinced this is sufficient reasoning. Why do you need a lua event to communicate with your own Lua layer? Why can't you call the Lua functions directly?


* ``onSetTriggerState(workshop,state)``
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* ``onSetTriggerState(workshop,state)``
* ``onSetTriggerState(workshop, state)``


Notify when building is triggered from linked lever or trap.

:workshop: object of type ``df.building_workshopst`` that is triggered.
:state: integer value of new state.

Examples
--------

Simple mechanical workshop::

require('plugins.building-hacks').registerBuilding{name="BONE_GRINDER",
consume=15,
gears={x=0,y=0}, --connection point
animate={
isMechanical=true, --animate the same conn. point as vanilla gear
frames={
{{x=0,y=0,42,7,0,0}}, --first frame, 1 changed tile
{{x=0,y=0,15,7,0,0}} -- second frame, same
local bld=require('plugins.building-hacks')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
local bld=require('plugins.building-hacks')
local bhacks = require('plugins.building-hacks')

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bld is consistently used throughout DFHack to refer to "current building". using the name to refer to the module would be confusing

--work only powered, consume 15 power and one connection point at 0,0
bld.setMachineInfo("BONE_GRINDER",true,15,0,{{x=0,y=0}})
--set animation to switch between gear tiles at 0,0
bld.setAnimationInfo("BONE_GRINDER",{
{[0]={42,7,0,0}}, --first frame, 1 changed tile
{[0]={15,7,0,0}} -- second frame, same
}
}
)

Or with auto_gears::

require('plugins.building-hacks').registerBuilding{name="BONE_GRINDER",
consume=15,
auto_gears=true
}
local bld=require('plugins.building-hacks')
bld.setMachineInfoAuto("BONE_GRINDER",true,15)
bld.setAnimationInfoAuto("BONE_GRINDER",true)

buildingplan
============
Expand Down
2 changes: 1 addition & 1 deletion docs/plugins/building-hacks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ building-hacks

.. dfhack-tool::
:summary: Provides a Lua API for creating powered workshops.
:tags: unavailable
:tags: fort gameplay buildings
:no-command:

See `building-hacks-api` for more details.
2 changes: 1 addition & 1 deletion plugins/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ if(BUILD_SUPPORTED)
dfhack_plugin(autoslab autoslab.cpp)
dfhack_plugin(blueprint blueprint.cpp LINK_LIBRARIES lua)
dfhack_plugin(burrow burrow.cpp LINK_LIBRARIES lua)
#dfhack_plugin(building-hacks building-hacks.cpp LINK_LIBRARIES lua)
dfhack_plugin(building-hacks building-hacks.cpp LINK_LIBRARIES lua)
add_subdirectory(buildingplan)
dfhack_plugin(changeitem changeitem.cpp)
dfhack_plugin(changelayer changelayer.cpp)
Expand Down