diff --git a/changelog.txt b/changelog.txt index d3c3e8a..3a5c998 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,10 +1,38 @@ +v1.0.2: Magma Forge + +Fixed the vagueness of most exceptions in the raws and pydwarf packages, most of them should now be reasonably forthcoming about what went wrong and why. +Fixed bug in raws.token.add which was arising from a dumb typo and really should have been caught long before. +Renamed raws.dir.addfile to raws.dir.add and raws.dir.setfile to raws.dir.set. +Renamed raws.file to raws.rawfile to make things less confusing in light of some other additions. +Renamed stal.armourypack to stal.armoury because why not. +Removed pineapple.utils.addreaction, which has been superseded by the newer and shinier pineapple.utils.addobject. +Added a great deal of functionality to the raws.dir object, which is now capable of tracking an entire DF directory as opposed to just its raw/objects subdirectory. +Added three new file classes: raws.basefile, raws.reffile, and raws.binfile. +Added more documentation and tutorials, big surprise. (Also added and neglected to document a bunch of new functionality while I was at it, he he he.) +Added a number of handy new operator overloads and fiddled a few existing ones while I was at it. +Added methods like raws.queryable.removefirst, raws.queryable.removeall, and quite a few more. +Added script pkdawson.vegan, a port of df-vegan. +Added scripts underneath omniclasm.decay, a port of State of Decay. +Added scripts umiman.smallthings.threats and umiman.smallthings.nofamily, ports of parts of smallthings that PyDwarf couldn't support until now. +Added scripts pineapple.easypatch, pineapple.utils.addobject, pineapple.utils.addobjects, pineapple.utils.permitobject, pineapple.utils.permitobjects, and pineapple.utils.addhack. +Added raws.objects module which helps with knowing some things about how raws files are supposed to be structured. +Added a very nifty raws.tokenlist.each method which did great wonders for terseness of some mods. +Improved general flow and structure of manager code against pydwarf.session code, moved things around and generally tidied up. +Improved the __getitem__ method for raws.queryable objects so that now it can handle ellipses and slices and some other stuff too. +Improved the way token arguments are internally handled, and made a few changes to pertinent token methods. +Improved pineapple.utils.addentity and made a pretty big change to how it accepts arguments in the process. +Improved raws.queryable.getobj and raws.queryable.allobj methods, now they can do even more stuff than before. +Improved the raws.tokenfilter constructor, which is now decent enough to handle passing a single tuple/whatever to exact_arg, re_arg, or arg_in rather than an iterable of them. +Improved stal.armoury, putnam.materialsplus, putnam.microreduce, shukaro.higherlearning, and shukaro.creationforge by rewriting each of them entirely. +Tweaked almost every script in some way, really, mostly in the interest of utilizing newer and more awesome functionality. + v1.0.1: Strange Mood Fixed pineapple.boneflux reaction by being less bad at string formatting Fixed pineapple.utils.addreaction not adding an OBJECT:REACTION token to created files Fixed new grasses in pineapple.cavegrass not being recognized by DF Fixed a small issue with logging in pineapple.cavegrass -Fixed non-raws file from input directory not being written to output +Fixed non-raws files from input directory not being written to output Fixed pydwarf.response casting to string always returning a string starting with "SUCCESS" even when unsuccessful Added a lot more documentation Added a shitty WIP tool for outputting docstrings as more readable html diff --git a/config.json b/config.json index 233a1e9..ab4278f 100644 --- a/config.json +++ b/config.json @@ -2,11 +2,14 @@ "comment": "This is an example file! Before running the manager you will need to change the provided paths to better suit your particular setup.", - "input": "E:/Sophie/Desktop/Files/Games/Dwarf Fortress/df_40_24_win/rawvanilla/objects", - "output": "E:/Sophie/Desktop/Files/Games/Dwarf Fortress/df_40_24_win/raw/objects", - "backup": "E:/Sophie/Desktop/Files/Games/Dwarf Fortress/df_40_24_win/rawbak/", + "input": "E:/Sophie/Desktop/Files/Games/Dwarf Fortress/df_40_24_win/", + "output": "E:/Sophie/Desktop/Files/Games/Dwarf Fortress/df_40_24_win/pydwarf_output", + "backup": "E:/Sophie/Desktop/Files/Games/Dwarf Fortress/df_40_24_win/pydwarf_backup", + + "paths": "auto", "version": "auto", + "hackversion": "auto", "scripts": [ { diff --git a/config/__init__.py b/config/__init__.py deleted file mode 100644 index cbe96ec..0000000 --- a/config/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from config import * - -__version__ = '1.0.0' diff --git a/config/config.py b/config/config.py deleted file mode 100644 index d500be2..0000000 --- a/config/config.py +++ /dev/null @@ -1,86 +0,0 @@ -import os -import sys - -sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) - -import logging -import json -import importlib -from datetime import datetime -import pydwarf - - - -# Used in some file paths and such -datetimeformat = '%Y.%m.%d.%H.%M.%S' -timestamp = datetime.now().strftime(datetimeformat) - - - -class config: - def __init__(self, version=None, input=None, output=None, backup=None, scripts=[], packages=[], verbose=False, log='logs/%s.txt' % timestamp): - self.version = version # Dwarf Fortress version, for handling script compatibility metadata - self.input = input # Raws are loaded from this input directory - self.output = output # Raws are written to this output directory - self.backup = backup # Raws are backed up to this directory before any changes are made - self.scripts = scripts # These scripts are run in the order that they appear - self.packages = packages # These packages are imported (probably because they contain PyDwarf scripts) - self.verbose = verbose # Log DEBUG messages to stdout if True, otherwise only INFO and above - self.log = log # Log file goes here - - def json(self, path, *args, **kwargs): - with open(path, 'rb') as jsonfile: return self.apply(json.load(jsonfile), *args, **kwargs) - - def apply(self, data, applynone=False): - if data: - for key, value in data.iteritems(): - if applynone or value is not None: self.__dict__[key] = value - return self - - def __str__(self): - return str(self.__dict__) - def __repr__(self): - return self.__str__() - - def setup(self): - # Set up the pydwarf logger - self.setuplogger() - # Handle version == 'auto' - self.setupversion() - # Import packages - self.setuppackages() - - def setuplogger(self): - # Set up the logger (And you should do it first thing!) - pydwarf.log.setLevel(logging.DEBUG) - # Handler for console output - stdouthandler = logging.StreamHandler(sys.stdout) - stdouthandler.setLevel(logging.DEBUG if self.verbose else logging.INFO) - stdouthandler.setFormatter(logging.Formatter('%(asctime)s: %(levelname)s: %(message)s', datetimeformat)) - pydwarf.log.addHandler(stdouthandler) - # Handler for log file output - if self.log: - logdir = os.path.dirname(self.log) - if not os.path.exists(logdir): os.makedirs(logdir) - logfilehandler = logging.FileHandler(self.log) - logfilehandler.setLevel(logging.DEBUG) - logfilehandler.setFormatter(logging.Formatter('%(asctime)s: %(filename)s[%(lineno)s]: %(levelname)s: %(message)s', datetimeformat)) - pydwarf.log.addHandler(logfilehandler) - - def setuppackages(self): - self.importedpackages = [importlib.import_module(package) for package in self.packages] - - def setupversion(self): - # Handle automatic version detection - if self.version == 'auto': - pydwarf.log.info('Attempting to automatically detect Dwarf Fortress version.') - self.version = pydwarf.detectversion(paths=(self.input, self.output), log=pydwarf.log) - if self.version == None: - pydwarf.log.info('Unable to detect Dwarf Fortress version.') - else: - pydwarf.log.info('Detected Dwarf Fortress version %s.' % self.version) - elif self.version is None: - pydwarf.log.warning('No Dwarf Fortress version was specified. Scripts will be run regardless of their indicated compatibility.') - else: - pydwarf.log.info('Managing Dwarf Fortress version %s.' % self.version) - pydwarf.urist.session.dfversion = self.version diff --git a/config_override.py b/config_override.py index b118b19..23095ef 100644 --- a/config_override.py +++ b/config_override.py @@ -13,7 +13,9 @@ 'input': os.path.join(dfdir, 'vanillaraw/objects'), 'output': 'output', 'backup': 'backup', + 'paths': 'auto', 'version': 'auto', + 'hackversion': 'auto', 'scripts': [ { 'name': 'pineapple.deerappear', @@ -36,3 +38,4 @@ else: export = {} + diff --git a/docs/build.py b/docs/bin/build.py similarity index 97% rename from docs/build.py rename to docs/bin/build.py index b656f3a..cd0f558 100644 --- a/docs/build.py +++ b/docs/bin/build.py @@ -3,14 +3,14 @@ import sys import os -sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..')) +sys.path.append(os.path.join(os.path.dirname(os.path.abspath(__file__)), '../..')) import inspect import raws import pydwarf -output = 'index.html' +output = '../index.html' items = { 'raws.token': raws.token, diff --git a/docs/bin/scripts.bat b/docs/bin/scripts.bat new file mode 100644 index 0000000..36d56d9 --- /dev/null +++ b/docs/bin/scripts.bat @@ -0,0 +1,2 @@ +cd ../../ +python manager.py --meta --metaformat "md" --writedoc "docs/scripts.md" diff --git a/docs/bin/scripts.sh b/docs/bin/scripts.sh new file mode 100644 index 0000000..36d56d9 --- /dev/null +++ b/docs/bin/scripts.sh @@ -0,0 +1,2 @@ +cd ../../ +python manager.py --meta --metaformat "md" --writedoc "docs/scripts.md" diff --git a/docs/configuring.md b/docs/configuring.md new file mode 100644 index 0000000..0b12929 --- /dev/null +++ b/docs/configuring.md @@ -0,0 +1,27 @@ +# Configuring PyDwarf + +The steps given here are specifically for Windows, but the procedure on other operating systems will be almost identical. + +- First thing, ensure that [Python 2.7](https://www.python.org/download/releases/2.7.8/) is installed on your computer. If you have another version of Python already installed this can be a little complicated but there are lots of [helpful resources](http://stackoverflow.com/questions/4583367/how-to-run-multiple-python-version-on-windows) available to guide you through it. + +- In order to keep everything working as smoothly as possible, you should copy your Dwarf Fortress directory to another location before messing about with PyDwarf. Navigate to the directory containing your Dwarf Fortress folder, copy it, and paste it somewhere. It may be easiest to place the copy in the same location and append `_original` to the end of the directory's name. After doing this you might, for example, have a folder at `C:/df_40_24_win` and another at `C:/df_40_24_win_original`. + +- If you haven't already, you'll need to download PyDwarf and extract the archive somewhere. It much doesn't matter where, though I recommend you *don't* put it inside your Dwarf Fortress directory. The most important files located in here are named `manager.py`, which is for actually running PyDwarf, and `config.json`, for telling it precisely what to do when it runs. + +- You'll want to open the `config.json` file, located in PyDwarf's root directory, with a text editor such as Notepad. And then you'll be looking at a JSON file. It assigns several parameters in the format of `"name": value,` and the most important ones right now are the ones named `input`, `output`, `backup`, and `scripts`. You can see that most of the values are text information enclosed within quotes, but `scripts` in particular is assigned a list of values contained within square brackets. + +- Set the value for `input`, which is a file path, to the location of that copy of Dwarf Fortress you made in a previous step. This tells PyDwarf where to read your files from so that they can be worked upon by various mods. For example, this file path might be something like `C:/df_40_24_win_original`. + +- And set the value for `output`, which is also a file path, to the location of the Dwarf Fortress folder that you play with. This is where PyDwarf will write your files to when it's finished modifying them. This path might look like `C:/df_40_24_win`. + +- As well as the value for `backup`, another file path, to somewhere for the Dwarf Fortress files to be backed up to. This could be something like the name of the `input` folder with `_backup` appended. This helps to ensure that if something weird goes wrong - and don't worry, it really shouldn't - you'll still have a copy of your original files lying around somewhere. The path might look like `C:/df_40_24_win_backup`. + +- And the really fun part is the `scripts` parameter. Here names of scripts are given in the order that they should be run. It's also possible to pass arguments to scripts here, which change the way it behaves. One way to get a list of the available scripts is to run `python manager.py --list`, and one way to see documentation regarding one of these scripts to describe its purpose and usage is to run `python manager.py --meta script.name`. + +- For the sake of example, you can try adding an item to the end of the `scripts` list, the text (including quotes) `"pineapple.subplants"`. In doing so, be sure to add a comma to the end of the previous line, these commas serve to separate items in the list. + +- Installing new mods for PyDwarf is really simple, if you want to use one that didn't come packaged with it. Simply place the uncompressed files, which should include at least one with a name like `pydwarf.scriptname.py`, anywhere in the `scripts` directory located within PyDwarf's root directory. If you had installed a mod named `this.is.a.script`, for example, you could tell PyDwarf to include it by adding it in the same way as the previous script: In the `scripts` list of `config.json`, you'd need to add a line to the list that looks like `"this.is.a.script"`. + +- And finally, to actually run PyDwarf and apply the mods, run `python manager.py`. It will helpfully tell you if anything went wrong and, if so, what exactly happened. But PyDwarf is a piece of work and sometimes those errors may be hard to understand. If you're not sure how to fix it, you can always post in the [GitHub issue tracker](https://github.com/pineapplemachine/PyDwarf/issues) or the [Bay12 forum topic](http://www.bay12forums.com/smf/index.php?topic=150857.msg6239158#msg6239158) to ask for help. + +- After running `manager.py`, if everything went smoothly, you're all done! Now you can run Dwarf Fortress and generate a new world to see the changes take effect. diff --git a/docs/index.html b/docs/index.html index f2f3375..8ecf0a2 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,12 +1,12 @@ -PyDwarf docs

Warning: This is a shitty WIP.

pydwarf.session

__init__

None

eval

None

failed

None

funcs

None

handle

None

handleall

None

inlist

None

successful

None

pydwarf.urist

__call__

None

__hash__

None

__init__

None

allregistered

None

cullcandidates

None

cullcandidates_compatibility

None

cullcandidates_dependency

None

cullcandidates_duplicates

None

cullcandidates_match

None

depsatisfied

None

doc

Make a pretty metadata string.

doclist

None

forfunc

None

get

None

getfn

None

getname

None

getregistered

None

info

None

list

None

matches

None

meta

None

splitname

None

raws.boolfilter

__and__

None

__contains__

None

__init__

None

__invert__

None

__or__

None

__xor__

None

all

None

any

None

basematch

None

copy

None

invert

None

inverted

None

match

None

none

None

one

None

raws.color

black

None

blue

None

brown

None

color

None

cyan

None

dgray

None

green

None

lblue

None

lcyan

None

lgray

None

lgreen

None

lmagenta

None

lred

None

magenta

None

red

None

white

None

yellow

None

raws.dir

__class__

type(object) -> the object's type
-type(name, bases, dict) -> a new type

__contains__

None

__delattr__

x.__delattr__('name') <==> del x.name

__format__

default object formatter

__getattribute__

x.__getattribute__('name') <==> x.name

__getitem__

None

__hash__

x.__hash__() <==> hash(x)

__init__

Constructor for rawsdir object.

__iter__

None

__new__

T.__new__(S, ...) -> a new object with type S, a subtype of T

__reduce__

helper for pickle

__reduce_ex__

helper for pickle

__setattr__

x.__setattr__('name', value) <==> x.name = value

__setitem__

None

__sizeof__

__sizeof__() -> int
+PyDwarf docs

Warning: This is a shitty WIP.

pydwarf.session

__init__

None

backup

None

configure

None

eval

None

failed

None

funcs

None

handle

None

handleall

None

inlist

None

successful

None

write

None

pydwarf.urist

__call__

None

__hash__

None

__init__

None

allregistered

None

cullcandidates

None

cullcandidates_compatibility

None

cullcandidates_dependency

None

cullcandidates_duplicates

None

cullcandidates_match

None

depsatisfied

None

doc

Make a pretty metadata string.

doclist

None

forfunc

None

get

None

getfn

None

getname

None

getregistered

None

info

None

list

None

matches

None

meta

None

splitname

None

raws.boolfilter

__and__

None

__contains__

None

__init__

None

__invert__

None

__or__

None

__xor__

None

all

None

any

None

basematch

None

copy

None

invert

None

inverted

None

match

None

none

None

one

None

raws.color

black

None

blue

None

brown

None

color

None

cyan

None

dgray

None

green

None

lblue

None

lcyan

None

lgray

None

lgreen

None

lmagenta

None

lred

None

magenta

None

red

None

white

None

yellow

None

raws.dir

__class__

type(object) -> the object's type
+type(name, bases, dict) -> a new type

__contains__

None

__delattr__

x.__delattr__('name') <==> del x.name

__enter__

None

__exit__

None

__format__

default object formatter

__getattribute__

x.__getattribute__('name') <==> x.name

__getitem__

None

__hash__

x.__hash__() <==> hash(x)

__init__

Constructor for rawsdir object.

__iter__

None

__new__

T.__new__(S, ...) -> a new object with type S, a subtype of T

__reduce__

helper for pickle

__reduce_ex__

helper for pickle

__setattr__

x.__setattr__('name', value) <==> x.name = value

__setitem__

None

__sizeof__

__sizeof__() -> int
 size of object in memory, in bytes

__subclasshook__

Abstract classes can override this to customize issubclass().
 
 This is invoked early on by abc.ABCMeta.__subclasscheck__().
 It should return True, False or NotImplemented.  If it returns
 NotImplemented, the normal algorithm is used.  Otherwise, it
 overrides the normal algorithm (and the outcome is cached).
-

addfile

None

addpath

None

all

None

allobj

Gets all objects matching a given type and optional id or id regex.
+

add

None

addbyauto

None

addbybincontent

None

addbydir

None

addbydirpath

None

addbyfile

None

addbyfilepath

None

addbyname

None

addbytokens

None

addfile

Deprecated: As of v1.0.2. Use the add method instead.

addfilestodicts

None

addfiletodicts

Internal: Used to add a file to files and filenames dictionaries.

addtodicts

None

all

None

allobj

Gets all objects matching a given type and optional id or id regex.
         
         Example usage:
             >>> pants = df.allobj('ITEM_PANTS')
@@ -39,11 +39,11 @@
             >>> print hematite.allprop('ENVIRONMENT') # Gets only the ENVIRONMENT tokens belonging to hematite
             [ENVIRONMENT:SEDIMENTARY:VEIN:100]
             [ENVIRONMENT:IGNEOUS_EXTRUSIVE:VEIN:100]
-        

alluntil

None

argsprops

None

argstokens

None

argsuntil

None

get

None

getfile

Gets the file with a given name. If no file by that name is found,
+        

alluntil

None

argsprops

None

argstokens

None

argsuntil

None

clean

None

filebyfilepath

None

filebyname

None

filesbydir

None

filesbydirpath

None

get

None

getdestforfileop

Internal

getfile

Gets the file with a given name. If no file by that name is found,
         None is returned instead. If creature is set to something other than
         None, the behavior when no file by some name exists is altered: A new
         file is created and associated with that name, and then its add
-        method is called using the value for create as its argument.

getlast

None

getlastprop

Gets the last token matching the arguments, but stops at the next
+        method is called using the value for create as its argument.

getitems

None

getlast

None

getlastprop

Gets the last token matching the arguments, but stops at the next
         token with the same value as this one. Should be sufficient in almost
         all cases to get a token representing a property of an object, when
         this method is called for a token representing an object. **kwargs
@@ -105,7 +105,7 @@
             [WAFERS]
             >>> print iron.getprop('WAFERS') # Stops at the next INORGANIC token, doesn't pick up adamantine's WAFERS token
             None
-        

getuntil

None

islice

None

list

Convenience method acts as a shortcut for raws.tokenlist(obj.tokens(*args, **kwargs)).
+        

getuntil

None

headersfortype

None

islice

None

iterfiles

None

list

Convenience method acts as a shortcut for raws.tokenlist(obj.tokens(*args, **kwargs)).
         
         Example usage:
             >>> elf = df.getobj('CREATURE:ELF')
@@ -152,16 +152,30 @@
             [TILE:156]
             >>> print props.get('NOT_A_TOKEN')
             None
-        

query

None

read

Reads raws from all text files in the specified directory.

removefile

None

setfile

None

slice

None

tokens

Iterate through all tokens.

until

None

write

Writes raws to the specified directory.

raws.file

__class__

type(object) -> the object's type
-type(name, bases, dict) -> a new type

__contains__

None

__delattr__

x.__delattr__('name') <==> del x.name

__eq__

None

__format__

default object formatter

__getattribute__

x.__getattribute__('name') <==> x.name

__getitem__

None

__hash__

x.__hash__() <==> hash(x)

__init__

Constructs a new raws file object.
-        
-        header: The header string to appear at the top of the file. Also used to determine filename.
+        

query

None

read

Reads raws from all text files in the specified directory.

remove

None

removeall

None

removeallobj

None

removeallprop

None

removealluntil

None

removefile

Deprecated: As of v1.0.2. Use the remove method instead.

removefirst

None

removefirstuntil

None

removelast

None

removelastprop

None

removelastuntil

None

removeobj

None

removeprop

None

removeselfandprops

None

removeuntil

None

slice

None

tokens

Iterate through all tokens.

until

None

write

Writes raws to the specified directory.

raws.file

__class__

type(object) -> the object's type
+type(name, bases, dict) -> a new type

__contains__

None

__delattr__

x.__delattr__('name') <==> del x.name

__enter__

None

__eq__

None

__exit__

None

__format__

default object formatter

__ge__

None

__getattribute__

x.__getattribute__('name') <==> x.name

__getitem__

Overrides object[...] behavior. Accepts a number of different types for the item argument, each resulting in different behavior.
+        
+        object[...]
+            Returns the same as object.list().
+        object[str]
+            Returns the same as object.get(str).
+        object[int]
+            Returns the same as object.index(int).
+        object[slice]
+            Returns the same as object.slice(slice).
+        object[iterable]
+            Returns a flattened list containing object[member] in order for each member of iterable.
+        object[anything else]
+            Raises an exception.
+        

__gt__

None

__hash__

None

__init__

Constructs a new raws file object.
+        
+        name: The name string to appear at the top of the file. Also used to determine filename.
         data: A string to be parsed into token data.
         path: A path to the file from which this object is being parsed, if any exists.
         tokens: An iterable of tokens from which to construct the object; these tokens will be its initial contents.
-        rfile: A file-like object from which to automatically read the header and data attributes.
+        file: A file-like object from which to automatically read the name and data attributes.
         dir: Which raws.dir object this file belongs to.
-        

__iter__

None

__len__

None

__ne__

None

__new__

T.__new__(S, ...) -> a new object with type S, a subtype of T

__reduce__

helper for pickle

__reduce_ex__

helper for pickle

__setattr__

x.__setattr__('name', value) <==> x.name = value

__sizeof__

__sizeof__() -> int
+        

__iter__

None

__le__

None

__len__

None

__lt__

None

__ne__

None

__new__

T.__new__(S, ...) -> a new object with type S, a subtype of T

__nonzero__

None

__reduce__

helper for pickle

__reduce_ex__

helper for pickle

__setattr__

x.__setattr__('name', value) <==> x.name = value

__sizeof__

__sizeof__() -> int
 size of object in memory, in bytes

__subclasshook__

Abstract classes can override this to customize issubclass().
 
 This is invoked early on by abc.ABCMeta.__subclasscheck__().
@@ -183,8 +197,7 @@
             [ITEM_FOOD:ITEM_FOOD_ROAST]
             [NAME:roast]
             [LEVEL:4]
-            >>> tokens = item_food.add('
-hi! [THIS][IS][AN][EXAMPLE]')
+            >>> tokens = item_food.add('hi! [THIS][IS][AN][EXAMPLE]')
             >>> print tokens
             hi! [THIS][IS][AN][EXAMPLE]
             >>> print item_food.list()
@@ -197,8 +210,7 @@
             [LEVEL:3]
             [ITEM_FOOD:ITEM_FOOD_ROAST]
             [NAME:roast]
-            [LEVEL:4]
-            hi! [THIS][IS][AN][EXAMPLE]
+            [LEVEL:4]hi! [THIS][IS][AN][EXAMPLE]
         

all

None

allobj

Gets all objects matching a given type and optional id or id regex.
         
         Example usage:
@@ -232,7 +244,7 @@
             >>> print hematite.allprop('ENVIRONMENT') # Gets only the ENVIRONMENT tokens belonging to hematite
             [ENVIRONMENT:SEDIMENTARY:VEIN:100]
             [ENVIRONMENT:IGNEOUS_EXTRUSIVE:VEIN:100]
-        

alluntil

None

argsprops

None

argstokens

None

argsuntil

None

clear

Remove all tokens from this file.
+        

alluntil

None

argsprops

None

argstokens

None

argsuntil

None

bin

None

clear

Remove all tokens from this file.
         
         Example usage:
             >>> item_pants = df.getfile('item_pants')
@@ -241,7 +253,7 @@
             >>> item_pants.clear()
             >>> print item_pants.length()
             0
-        

copy

Makes a copy of a file and its contents.
+        

content

None

copy

Makes a copy of a file and its contents.
         
         Example usage:
             >>> item_food = df.getfile('item_food')
@@ -265,17 +277,7 @@
             [LEVEL:4][EXAMPLE:TOKEN]
             >>> print item_food == food_copy
             False
-        

equals

None

get

None

getheader

Get the file header.
-        
-        Example usage:
-            >>> dwarf = df.getobj('CREATURE:DWARF')
-            >>> creature_standard = dwarf.file
-            >>> print creature_standard.getheader()
-            creature_standard
-            >>> creature_standard.setheader('example_header')
-            >>> print creature_standard.getheader()
-            example_header
-        

getlast

None

getlastprop

Gets the last token matching the arguments, but stops at the next
+        

dest

Internal: Given a root directory that this file would be written to, get the full path of where this file belongs.

equals

None

factory

None

get

None

getitems

None

getlast

None

getlastprop

Gets the last token matching the arguments, but stops at the next
         token with the same value as this one. Should be sufficient in almost
         all cases to get a token representing a property of an object, when
         this method is called for a token representing an object. **kwargs
@@ -287,7 +289,17 @@
             [ITEMS_SOFT]
             >>> print iron.getlastprop(re_value='ITEMS_.+') # Gets the last ITEMS_ token which belongs to iron
             [ITEMS_SCALED]
-        

getlastuntil

None

getobj

Get the first object token matching a given type and id. (If there's more 
+        

getlastuntil

None

getname

Get the file name.
+        
+        Example usage:
+            >>> dwarf = df.getobj('CREATURE:DWARF')
+            >>> creature_standard = dwarf.file
+            >>> print creature_standard.getname()
+            creature_standard
+            >>> creature_standard.setheader('example_header')
+            >>> print creature_standard.getname()
+            example_header
+        

getobj

Get the first object token matching a given type and id. (If there's more 
             than one result for any given query then I'm afraid you've done something
             silly with your raws.) This method should work properly with things like
             CREATURE:X tokens showing up in entity_default. Should almost always be
@@ -318,7 +330,7 @@
             [WAFERS]
             >>> print iron.getprop('WAFERS') # Stops at the next INORGANIC token, doesn't pick up adamantine's WAFERS token
             None
-        

getuntil

None

index

None

islice

None

length

Get the number of tokens in this file.
+        

getuntil

None

headersfortype

None

index

None

islice

None

length

Get the number of tokens in this file.
         
         Example usage:
             >>> print df.getfile('creature_standard').length()
@@ -374,7 +386,7 @@
             [TILE:156]
             >>> print props.get('NOT_A_TOKEN')
             None
-        

query

None

read

Internal: Given a file-like object, reads header and data from it.

remove

Remove this file from the raws.dir object to which it belongs.
+        

query

None

raw

None

read

Given a path or file-like object, reads name and data.

ref

None

reloc

None

remove

Remove this file from the raws.dir object to which it belongs.
         
         Example usage:
             >>> dwarf = df.getobj('CREATURE:DWARF')
@@ -387,21 +399,21 @@
             None
             >>> print df.getfile('creature_standard')
             None
-        

root

Gets the first token in the file.
+        

removeall

None

removeallobj

None

removeallprop

None

removealluntil

None

removefirst

None

removefirstuntil

None

removelast

None

removelastprop

None

removelastuntil

None

removeobj

None

removeprop

None

removeselfandprops

None

removeuntil

None

root

Gets the first token in the file.
         
         Example usage:
             >>> creature_standard = df.getfile('creature_standard')
             >>> print creature_standard.root()
             [OBJECT:CREATURE]
-        

setheader

Set the file header.
+        

setname

Set the file name.
         
         Example usage:
             >>> dwarf = df.getobj('CREATURE:DWARF')
             >>> creature_standard = dwarf.file
-            >>> print creature_standard.getheader()
+            >>> print creature_standard.getname()
             creature_standard
             >>> creature_standard.setheader('example_header')
-            >>> print creature_standard.getheader()
+            >>> print creature_standard.getname()
             example_header
         

setpath

None

settokens

Internal: Utility method for setting the root and tail tokens given an iterable.

slice

None

tail

Gets the last token in the file.
         
@@ -414,7 +426,7 @@
         reverse: If False, starts from the first token and iterates forwards. If True,
             starts from the last token and iterates backwards. Defaults to False.
         **kwargs: Other named arguments are passed on to the raws.token.tokens method.
-        

until

None

write

Internal: Given a file-like object, writes the file's contents to that file.

raws.token

__add__

Concatenates and returns a raws.tokenlist object.
+        

until

None

write

Given a path to a directory or a file-like object, writes the file's contents to that file.

raws.token

__add__

Concatenates and returns a raws.tokenlist object.
         
         Example usage:
             >>> one = raws.token('NUMBER:ONE')
@@ -451,7 +463,21 @@
             False
             >>> print elf >= elf
             True
-        

__getattribute__

x.__getattribute__('name') <==> x.name

__getitem__

None

__gt__

Returns True if this token appears after the other token in a file.
+        

__getattribute__

x.__getattribute__('name') <==> x.name

__getitem__

Overrides object[...] behavior. Accepts a number of different types for the item argument, each resulting in different behavior.
+        
+        object[...]
+            Returns the same as object.list().
+        object[str]
+            Returns the same as object.get(str).
+        object[int]
+            Returns the same as object.index(int).
+        object[slice]
+            Returns the same as object.slice(slice).
+        object[iterable]
+            Returns a flattened list containing object[member] in order for each member of iterable.
+        object[anything else]
+            Raises an exception.
+        

__gt__

Returns True if this token appears after the other token in a file.
         
         Example usage:
             >>> creature_standard = df.getfile('creature_standard')
@@ -461,7 +487,7 @@
             False
             >>> print elf < goblin
             True
-        

__hash__

None

__init__

None

__iter__

None

__le__

Returns True if this token appears before the other token in a file, or if this and the other refer to the same token.
+        

__hash__

None

__iadd__

None

__init__

None

__iter__

None

__le__

Returns True if this token appears before the other token in a file, or if this and the other refer to the same token.
         
         Example usage:
             >>> creature_standard = df.getfile('creature_standard')
@@ -520,7 +546,7 @@
             >>> token.addarg('hi!')
             >>> print token
             [EXAMPLE:hi!]
-        

addone

Internal: Utility method called by add when adding a single token

addprop

When this token is an object token like CREATURE:X or INORGANIC:X, a
+        

addargs

None

addone

Internal: Utility method called by add when adding a single token

addprop

When this token is an object token like CREATURE:X or INORGANIC:X, a
         new token is usually added immediately afterwards. However, if a token like
         COPY_TAGS_FROM or USE_MATERIAL_TEMPLATE exists underneath the object, then
         the specified tag is only added after that. **kwargs are passed on to the
@@ -554,11 +580,24 @@
             >>> print hematite.allprop('ENVIRONMENT') # Gets only the ENVIRONMENT tokens belonging to hematite
             [ENVIRONMENT:SEDIMENTARY:VEIN:100]
             [ENVIRONMENT:IGNEOUS_EXTRUSIVE:VEIN:100]
-        

alluntil

None

arg

When a token is expected to have only one argument, this method can be used
-        to access it. It there's one argument it will be returned, otherwise an
-        exception will be raised.
+        

alluntil

None

arg

When an index is given, the argument at that index is returned. If left
+        set to None then the first argument is returned if the token has exactly one
+        argument, otherwise an exception is raised.
         
         Example usage:
+            >>> token = raws.token('EXAMPLE:argument 0:argument 1')
+            >>> print token.getarg(0)
+            argument 0
+            >>> print token.getarg(1)
+            argument 1
+            >>> print token.getarg(2)
+            None
+            >>> print token.getarg(-1)
+            argument 1
+            >>> print token.getarg(-2)
+            argument 0
+            >>> print token.getarg(-3)
+            None
             >>> token_a = raws.token('EXAMPLE:x')
             >>> token_b = raws.token('EXAMPLE:x:y:z')
             >>> print token_a.arg()
@@ -574,7 +613,7 @@
             >>> token = raws.token('EXAMPLE:a:b:c')
             >>> print token.argsstr()
             a:b:c
-        

argstokens

None

argsuntil

None

auto

Internal: Convenience function for handling method arguments

containsarg

None

copy

Copies some token or iterable collection of tokens.
+        

argstokens

None

argsuntil

None

auto

Internal: Convenience function for handling method arguments

clearargs

None

containsarg

None

copy

Copies some token or iterable collection of tokens.
         
         Example usage:
             >>> token = raws.token('EXAMPLE:a:b:c')
@@ -623,25 +662,7 @@
             >>> tokens = raws.token.parse('[ONE][TWO][THREE][FOUR]')
             >>> print raws.token.firstandlast(tokens)
             ([ONE], [FOUR])
-        

follows

None

get

None

getarg

Gets argument at index, returns None if the index is out of bounds.
-        
-        index: The argument index.
-        
-        Example usage:
-            >>> token = raws.token('EXAMPLE:argument 0:argument 1')
-            >>> print token.getarg(0)
-            argument 0
-            >>> print token.getarg(1)
-            argument 1
-            >>> print token.getarg(2)
-            None
-            >>> print token.getarg(-1)
-            argument 1
-            >>> print token.getarg(-2)
-            argument 0
-            >>> print token.getarg(-3)
-            None
-        

getlast

None

getlastprop

Gets the last token matching the arguments, but stops at the next
+        

follows

None

get

None

getitems

None

getlast

None

getlastprop

Gets the last token matching the arguments, but stops at the next
         token with the same value as this one. Should be sufficient in almost
         all cases to get a token representing a property of an object, when
         this method is called for a token representing an object. **kwargs
@@ -703,25 +724,6 @@
                 [NAME:elf:elves:elven]
                 [CASTE_NAME:elf:elves:elven]
                 [CREATURE_TILE:'e'][COLOR:3:0:0]
-        

match

Returns True if this method matches some filter, false otherwise.
-        
-        filter: The filter to check, for example a raws.tokenfilter object.
-        **kwargs: Passed to the taws.tokenfilter constructor to nab a filter
-            to use if no filter was otherwise specified.
-        
-        Example usage:
-            >>> filter = raws.tokenfilter(exact_value='EXAMPLE')
-            >>> token_a = raws.token('HELLO:THERE')
-            >>> token_b = raws.token('EXAMPLE')
-            >>> token_c = raws.token('EXAMPLE:NUMBER:TWO')
-            >>> print token_a.match(filter)
-            False
-            >>> print token_b.match(filter)
-            True
-            >>> print token_c.match(filter)
-            True
-            >>> print token_a.match(exact_value='HELLO')
-            True
         

nargs

When count is None, returns the number of arguments the token has. (Length of
         arguments list.) Otherwise, returns True if the number of arguments is equal to the
         given count and False if not.
@@ -803,7 +805,7 @@
                 [TRANSLATION:ELF]
                 [WEAPON:ITEM_WEAPON_SPEAR]
                 [WEAPON:ITEM_WEAPON_BOW]
-        

sanitizeargstring

Internal: Utility method for sanitizing a string intended to be evaluated as an arugment for a token.

setarg

Sets argument at index, also verifies that the input contains no illegal characters.
+        

removeall

None

removeallprop

None

removealluntil

None

removefirst

None

removefirstuntil

None

removelast

None

removelastprop

None

removelastuntil

None

removeprop

None

removeselfandprops

None

removeuntil

None

setarg

Sets argument at index, also verifies that the input contains no illegal characters.
         If the index argument is set but not value, then the index is assumed to be referring to
         a value and the index is assumed to be 0.
         
@@ -819,7 +821,7 @@
             [EXAMPLE:a:b:500]
             >>> token.setarg('hi!')
             >>> print token
-            [EXAMPLE:hi!:b:500]

setprefix

Set the comment text preceding a token.
+            [EXAMPLE:hi!:b:500]

setargs

None

setprefix

Set the comment text preceding a token.
         
         value: The value to be set.
         
@@ -948,4 +950,4 @@
             to True then the query of which this filter is a member is made to terminated. If
             set to False, then this filter will only cease to accumulate results. Defaults to
             True.
-        

__invert__

None

__or__

None

__xor__

None

basematch

None

copy

None

invert

None

inverted

None

match

None

\ No newline at end of file +

__invert__

None

__or__

None

__xor__

None

anchor

None

basematch

None

copy

None

invert

None

inverted

None

match

None

\ No newline at end of file diff --git a/docs/scripts.md b/docs/scripts.md new file mode 100644 index 0000000..1e673d0 --- /dev/null +++ b/docs/scripts.md @@ -0,0 +1,551 @@ +# Scripts + +## omniclasm.decay.deteriorate.clothes + +Created by Omniclasm and Sophie Kirschner. + +This script is fairly straight forward. All of those slightly worn wool shoes that dwarves scatter all over the place will deteriorate at a greatly increased rate, and eventually just crumble into nothing. As warm and fuzzy as a dining room full of used socks makes your dwarves feel, your FPS does not like it. + +#### Arguments: + +* **auto_run:** If set to True then the script will be started automatically upon startup. + +## omniclasm.decay.deteriorate.corpses + +Created by Omniclasm and Sophie Kirschner. + +In long running forts, especially evil biomes, you end up with a lot of toes, teeth, fingers, and limbs scattered all over the place. Various corpses from various sieges, stray kitten corpses, probably some heads. This script causes all of those to rot away into nothing after several months. + +#### Arguments: + +* **auto_run:** If set to True then the script will be started automatically upon startup. + +## omniclasm.decay.deteriorate.food + +Created by Omniclasm and Sophie Kirschner. + +With this script running, all food and plants wear out and disappear after several months. Barrels and stockpiles will keep them from rotting, but it won't keep them from decaying. + +#### Arguments: + +* **auto_run:** If set to True then the script will be started automatically upon startup. + +## omniclasm.decay.starvingdead + +Created by Omniclasm and Sophie Kirschner. + +With this script running, all undead that have been on the map for a time (default: 1 month) start to gradually decay, losing strength, speed, and toughness. After they have been on the map for even longer (default: 3 months), they collapse upon themselves, never to be reanimated. + +#### Arguments: + +* **start:** Number of months before decay sets in. + +* **die:** Number of months before collapsing entirely. + +* **auto_run:** If set to True then the script will be started automatically upon startup. + +## pineapple.adoptsowner + +Created by Sophie Kirschner. + +todo + +#### Arguments: + +* **add_to:** If set to None, no ADOPTS_OWNER tokens are added. If set to '\*', tokens are added to all creatures. If set to an iterable containing IDs of creatures, ADOPTS_OWNER will be added to each of those creatures. Defaults to None. + +* **remove_from:** If set to None, no ADOPTS_OWNER tokens are removed. If set to '\*', all ADOPTS_OWNER tokens are removed. If set to an iterable containing IDs of creatures, ADOPTS_OWNER will be removed from each of those creatures. Defaults to '\*'. + +## pineapple.bauxitetoaluminum + +Created by Sophie Kirschner. + +Adds a reaction to the smelter to allow the creation of aluminum bars from bauxite (as ore) and cryolite (as flux). Credit to this forum discussion for the reaction and general inspiration: http://www.bay12forums.com/smf/index.php?topic=31523.0 + +#### Arguments: + +* **entities:** Adds the reaction to these entities. Defaults to only MOUNTAIN. + +* **aluminum_value:** Multiplies the MATERIAL_VALUE of aluminum by this much. Defaults to 0.75 to account for the increased availability of aluminum as a consequence of the new reaction. + +* **add_to_file:** Adds the reaction to this file. + +## pineapple.boneflux + +Created by Sophie Kirschner. + +Adds a reaction to the kiln which consumes bones and produces flux. Inspired by/stolen from Rubble's Bone Flux mod. + +#### Arguments: + +* **reaction_name:** The name of the reaction to be shown in the kiln. + +* **entities:** Adds the reaction to these entities. Defaults to MOUNTAIN and PLAINS. + +* **add_to_file:** Adds the reaction to this file. + +* **bone_count:** The number of bones required in the reaction. + +* **product_id:** ID of the boulder to get out of the reaction. Defaults to CALCITE. + +## pineapple.butcherinorganic + +Created by Sophie Kirschner. + +Allows butchering of some inorganics, get things like wood or stone from some corpses. Inspired by/stolen from Igfig's Modest Mod. + +#### Arguments: + +* **templates:** Associates material template names as keys with items as values. Each named template will be given a BUTCHER_SPECIAL:ITEM:NONE token, where ITEM is the value given. Defaults to adding logs, bars, and boulders to wood, metal, and stone templates respectively. + +## pineapple.castanvil + +Created by Sophie Kirschner. + +Adds a reaction to the smelter which makes it possible to create an iron anvil without already having a forge. Inspired by/stolen from Rubble's Cast Anvil mod. + +#### Arguments: + +* **entities:** Adds the reaction to these entities. Defaults to only MOUNTAIN. + +* **add_to_file:** Adds the reaction to this file. + +* **anvil_cost:** The cost in iron bars to create an anvil in this way. Defaults to 5. + +## pineapple.cavegrass + +Created by Sophie Kirschner. + +Changes the grasses in each cavern level to make the different levels more visually distinct, as well as adding a much greater variety. With default arguments the first cavern will be primarly green and yellow, the second blue and cyan, the third red and gray. Inspired by/stolen from Rubble's Cave Color mod. + +#### Arguments: + +* **grasses:** A dictionary specifying which grasses to change and modify. See the default_grasses dict for documentation. + +## pineapple.deerappear + +Created by Sophie Kirschner. + +Changes the appearance of each deer from a brown D to yellow d. + +#### Arguments: + +* **tile:** Set the tile that the deer's appeance will be set to. + +* **color:** Set the arguments that the deer's color token will be given. + +* **creature:** Change the creature whose appearance will be modified. (Heresy!) + +## pineapple.diff + +Created by Sophie Kirschner. + +Merges and applies changes made to some modded raws via diff checking. Should be reasonably smart about automatic conflict resolution but if it complains then I recommend giving things a manual checkover afterwards. Also, the token-based diff'ing approach should work much better than any line-based diff. Using this tool to apply mods made to other versions of Dwarf Fortress probably won't work so well. + +#### Arguments: + +* **paths:** Should be an iterable containing paths to individual raws files or to directories containing many. Files that do not yet exist in the raws will be added anew. Files that do exist will be compared to the current raws and the according additions/removals will be made. At least one path must be given. + +## pineapple.discipline + +Created by Sophie Kirschner. + +Applies natural discipline skill bonuses to creatures that should probably have them. Credit to Mictlantecuhtli for creating the mod which inspired this one. www.bay12forums.com/smf/index.php?topic=140460.0 + +#### Arguments: + +* **discipline_bonus:** A dict mapping property names to values: For each of these tokens that a creature possesses the corresponding bonuses are summed. The resulting value, rounded up, is used to determine the skill bonus. + +* **entity_bonus:** Handled separately, adds this value to the bonus for creatures which are listed as being members of any entity. + +* **badger_bonus:** Also handled separately, adds this skill bonus to badgers. + +## pineapple.easypatch + +Created by Sophie Kirschner. + +Given a path to a file, a directory, a content string, a tokenlist, a raws file object, or an iterable containing a combination of these, a file or files are added to the dir object, and these same objects can be permitted using the permitted_entities argument. + +#### Arguments: + +* **files:** The file or files to be added. + +* **\*\*kwargs:** Passed on to pineapple.utils.permitobjects. + +## pineapple.flybears + +Created by Sophie Kirschner. + +Example script which causes all female bears to fly. + +## pineapple.greensteel + +Created by Sophie Kirschner. + +Adds an alloy which is lighter and sharper than steel but not so much as adamantine. It can be made from similar ingredients as steel with the addition of adamantine bars or a new adamant ore. + +#### Arguments: + +* **entities:** The entities which should be permitted this reaction. Defaults to only MOUNTAIN. + +## pineapple.maxage + +Created by Sophie Kirschner. + +Applies a MAXAGE to most vanilla creatures which don't already have one. + +#### Arguments: + +* **output_needs_age:** When True, creatures that have no MAXAGE and aren't specified in the ages dict will be outputted to the log. Can maybe be helpful for debugging things. + +* **apply_default_age:** Most creatures that don't have a MAXAGE and aren't specified in the ages dict will have this default applied to their MAXAGE. It should be an iterable containing arguments same as values in the ages dict. This will not be applied to wagons, to megabeasts, to undead, or to nonexistent creatures. + +* **ages:** A dictionary mapping creature names as keys to what that creature's arguments should be for MAXAGE: That is, it should look like (minimum_lifespan, maximum_lifespan). + +## pineapple.metalitems + +Created by Sophie Kirschner. + +Allows the forging of every type of metal item from the specified metals. + +#### Arguments: + +* **items:** These are the items that the listed metals will always be allowed for. + +* **metals:** These metals will be made to allow forging of each item specified. + +## pineapple.noaquifers + +Created by Sophie Kirschner. + +Removes all AQUIFER tokens. + +## pineapple.noexotic + +Created by Sophie Kirschner. + +Replaces all [PET_EXOTIC] and [MOUNT_EXOTIC] tags with their non-exotic counterparts. + +## pineapple.nograzers + +Created by Sophie Kirschner. + +Removes all [GRAZER] and [STANDARD_GRAZER] tokens. + +## pineapple.nomaxage + +Created by Sophie Kirschner. + +Removes MAXAGE tokens from creatures. + +#### Arguments: + +* **apply_to_creatures:** Also removes MAXAGE from these creatures regardless of whether they possess any of the properties in required_property. Set to None to apply to no other creatures. Defaults to None. + +* **required_property:** An iterable containing token values, e.g. ('INTELLIGENT', 'CAN_LEARN'): for each creature having both a MAXAGE token and one or more of these tokens, that creature's MAXAGE token will be removed. If set to None, then no MAXAGE tokens will be removed in this way. If set to ['\*'], MAXAGE tokens will be removed from all creatures. + +## pineapple.orientation + +Created by Sophie Kirschner. + +Causes all creatures of some type to have a single sexuality, heterosexual being the default. (You boring snob!) + +#### Arguments: + +* **mode:** Accepts one of these strings as its value, or None: "hetero", the default, makes the creatures exclusively straight. "gay" makes the creatures exclusively gay. "bi" makes the creatures exclusively bisexual. "ace" makes the creatures exclusively asexual. Can alternatively be set as a custom tuple same as those found in the mode_info dict: The list/tuple should contain six values corresponding to (disinterest in the same gender, romantic (but not marriage) interest in the same, commitment to the same, disinterest in the other gender, romantic interest in the other, commitment to the other). + +* **creatures:** An iterable containing creatures whose sexuality should be affected. Set to None to affect all creatures. + +## pineapple.skillrust + +Created by Sophie Kirschner. + +Modifies skill rust for given creatures. Disables it entirely by default. + +#### Arguments: + +* **rates:** What the skill rust rates are to be changed to. It must be a tuple or list containing three values. The default is ('NONE', 'NONE', 'NONE'), which disables skill rust entirely. Dwarf Fortress's default rates are ('8', '16', '16'). Lower numbers indicate faster skill rust. + +* **creatures:** An iterable containing creatures for which to disable skill rust. + +## pineapple.stoneclarity + +Created by Sophie Kirschner. + +Allows powerful editing of the appearances of stone, ore, and gems. + +#### Arguments: + +* **rules:** By default makes all flux stone white, makes all fuel use \*, makes all ore use £ unmined and \* in stockpiles, makes cobaltite use % unmined and • in stockpiles, makes all gems use ☼. Specify an object other than default_rules to customize behavior, and refer to default_rules as an example of how rules are expected to be represented + +* **query:** This query is run for each inorganic found and looks for tokens that should be recognized as indicators that some inorganic belongs to some group. Refer to the default query for more information. + +* **fuels:** If left unspecified, stoneclarity will attempt to automatically detect which inorganics are fuels. If you know that no prior script added new inorganics which can be made into coke then you can cut down a on execution time by setting fuels to fuels_vanilla. + +## pineapple.subplants + +Created by Sophie Kirschner. + +Makes all subterranean plants grow year-round. + +## pineapple.useablemats + +Created by Sophie Kirschner. + +Causes scales, feathers, and chitin to become useful for crafting. Inspired by/stolen from Rubble's Usable Scale/Feathers/Chitin fixes. + +#### Arguments: + +* **scales:** Recognized when using the default options dict. If set to True, scales will be made to act more like leather for crafting purposes. + +* **feathers:** Recognized when using the default options dict. If set to True, feathers will be useable for making soft items, such as clothing. + +* **options:** A dictionary associating option names with tuples where the first element is the name of a MATERIAL_TEMPLATE and the second is tokens to be added to that template. Option names, when passed as a keyword argument and set to False, will cause that option to be disabled. + +* **chitin:** Recognized when using the default options dict. If set to True, chitin will be made to act more like shells for crafting purposes. + +## pineapple.utils.addhack + +Created by Sophie Kirschner. + +Utility script for adding a new DFHack script. + +#### Arguments: + +* **\*\*kwargs:** Other named arguments will be passed on to the dir.add method used to create the file object corresponding to the added script. + +* **auto_run:** If set to True, a line will be added to dfhack.init containing only the name of the added script. If set to None, no such line will be added. If set to an arbitrary string, that string will be added as a new line at the end of dfhack.init. + +## pineapple.utils.addobject + +Created by Sophie Kirschner. + +Utility script for adding a new object to the raws. + +#### Arguments: + +* **item_rarity:** Most items, when adding tokens to entities to permit them, accept an optional second argument specifying rarity. It should be one of 'RARE', 'UNCOMMON', 'COMMON', or 'FORCED'. This argument can be used to set that rarity. + +* **add_to_file:** The name of the file to add the object to. If it doesn't exist already then the file is created anew. The string is formatted such that %(type)s is replaced with the object_header, lower case. + +* **permit_entities:** For relevant object types such as reactions, buildings, and items, if permit_entities is specified then tokens are added to those entities to permit the added object. + +* **tokens:** The tokens belonging to the object to create. + +* **object_header:** When the object is added to a file which doesn't already exist, an [OBJECT:TYPE] token must be added at its beginning. This argument, if specified, provides the type in that token. Otherwise, when the argument is left set to None, the type will be automatically decided. + +* **type:** Specifies the object type. If type and id are left unspecified, the first token of the tokens argument is assumed to be the object's [TYPE:ID] token and the type and id arguments are taken out of that. + +* **id:** Specifies the object id. If type and id are left unspecified, the first token of the tokens argument is assumed to be the object's [TYPE:ID] token and the type and id arguments are taken out of that. + +## pineapple.utils.addobjects + +Created by Sophie Kirschner. + +Utility script for adding several new objects to the raws at once. + +#### Arguments: + +* **\*\*kwargs:** Passed on to pineapple.utils.addobject. + +* **add_to_file:** The name of the file to add the object to. If it doesn't exist already then the file is created anew. The string is formatted such that %(type)s is replaced with the object_header, lower case. + +* **objects:** An iterable containing tokens belonging to the objects to add. + +## pineapple.utils.addtoentity + +Created by Sophie Kirschner. + +A simple utility script which adds tokens to entities. + +#### Arguments: + +* **tokens:** A string or collection of tokens to add to each entity. + +* **entities:** Adds tokens to these entities. + +## pineapple.utils.objecttokens + +Created by Sophie Kirschner. + +Utility script for adding or removing tokens from objects. + +#### Arguments: + +* **add_to:** If set to None, no tokens tokens are added. If set to '\*', tokens are added to all objects. If set to an iterable containing IDs of objects, tokens will be added to each of those objects. + +* **token:** The token to be added or removed. + +* **object_type:** The type of object which should be affected. + +* **remove_from:** If set to None, no matching tokens are removed. If set to '\*', all matching tokens are removed. If set to an iterable containing IDs of objects, matching tokens will be removed from each of those objects. + +## pineapple.utils.permitobject + +Created by Sophie Kirschner. + +Utility script for permitting an object with entities. + +#### Arguments: + +* **permit_entities:** For relevant object types such as reactions, buildings, and items, if permit_entities is specified then tokens are added to those entities to permit the added object. + +* **type:** Specifies the object type. + +* **item_rarity:** Some items, when adding tokens to entities to permit them, accept an optional second argument specifying rarity. It should be one of 'RARE', 'UNCOMMON', 'COMMON', or 'FORCED'. This argument can be used to set that rarity. + +* **id:** Specifies the object id. + +## pineapple.utils.permitobjects + +Created by Sophie Kirschner. + +Utility script for permitting several objects at once with entities. + +#### Arguments: + +* **\*\*kwargs:** Passed on to pineapple.utils.permitobject. + +* **objects:** An iterable containing either tokens or type, id tuples representing objects to be permitted. + +## pineapple.woodmechanisms + +Created by Sophie Kirschner. + +Allows construction of wooden mechanisms at the craftdwarf's workshop. Inspired by/stolen from Rubble's Wooden Mechanisms mod. + +#### Arguments: + +* **entities:** Adds the reaction to these entities. Defaults to MOUNTAIN and PLAINS. + +* **add_to_file:** Adds the reaction to this file. + +* **log_count:** The number of logs required in the reaction. + +## pkdawson.vegan + +Created by Patrick Dawson and Sophie Kirschner. + +Adds reactions to the craftdwarf's workshop for making quivers and backpacks from cloth, which normally require leather. Also adds a DFHack script which disables non-vegan labors using autolabor. + +#### Arguments: + +* **lua_file:** The DFHack script will be added to this path, relative to DF's root directory. If set to None then no DFHack script will be written. + +* **entities:** Adds the reaction to these entities. Defaults to MOUNTAIN and PLAINS. + +* **add_to_file:** Adds the reaction to this file. + +* **labors:** These labors will be disabled using a DFHack script. If set to None then no DFHack script will be written. The default labors are BUTCHER, TRAPPER, DISSECT_VERMIN, LEATHER, TANNER, MAKE_CHEESE, MILK, FISH, CLEAN_FISH, DISSECT_FISH, HUNT, BONE_CARVE, SHEARER, BEEKEEPING, WAX_WORKING, GELD. + +* **auto_run:** If set to True, and if the DFHack script is added, then a line will be added to the end of dfhack.init which runs this script on startup. If set to False then the script will wait to be run manually. + +## putnam.materialsplus + +Created by Putnam and Sophie Kirschner. + +Adds a bunch of materials to the game. + +## putnam.microreduce + +Created by Putnam and Sophie Kirschner. + +A mod to reduce the amount of micromanagement in Dwarf Fortress. One-step soap making and clothesmaking! + +## shukaro.creationforge + +Created by Shukaro and Sophie Kirschner. + +This is a simple workshop I modded in to help test custom reactions, buildings, and creatures. It's used to create various different items so that you don't have to set up an entire fortress to test some reactions. Hopefully it's a useful tool to people, even if it's just to look at the raw formatting. + +## shukaro.higherlearning + +Created by Shukaro and Sophie Kirschner. + +Have you ever wondered to yourself, "Man, my dwarves are such idiots, I wish I could chisel some intelligence into their heads."? No? Then, er, disregard that last bit. What I present to you, here and now, no strings attached, is a workshop to solve a problem that you probably didn't even know you had! I call it, the Dwarven Higher Learning Mod. Now, what this thingawazzit does, is give your dwarves an opportunity to polish up some skills that they may have trouble practicing elsewhere. You know the situation, Urist McDoctor has your legendary axedwarf on your table, and he's got no idea how to stop him from bleeding out from wounds caused by rogue fluffy wamblers. Or you have precious little metal available on the glacier you so stupidly bravely embarked on, so you can't afford to waste it on dabbling weaponsmiths who've never handled a hammer before in their lives. This mod's workshops allow training through the time-honored traditions of; hitting rocks until your hands bleed, performing repetitive actions that will sap your will to live, practicing your skills on subjects that are worth less than most peasants (and won't sue for malpractice), and studying the works of your fellow dwarves, knowing full-well that you'll never be quite as good as them. + +#### Arguments: + +* **entities:** An iterable containing names of entities the workshops will be added to. Defaults to only MOUNTAIN. + +## smeeprocket.transgender + +Created by SmeepRocket and Sophie Kirschner. + +Adds transgender and intersex castes to creatures. + +#### Arguments: + +* **beards:** If True, all dwarf castes will be given beards. If False, none of the added castes will have beards for any species. Defaults to True. + +* **frequency:** Higher numbers cause rarer incidence of added castes. + +* **species:** An iterable containing each species that should be given transgender and intersex castes. + +## stal.armoury.attacks + +Created by Stalhansch and Sophie Kirschner. + +Removes attacks from creatures. By default, as a way to improve balance in combat, scratch and bite attacks are removed from dwarves, humans, and elves. + +#### Arguments: + +* **remove_attacks:** Removes these attacks from species listed in remove_attacks_from. Defaults to scratch and bite. + +* **remove_attacks_from:** If set to True, specified remove_attacks are removed from the species in the list to improve combat balancing. If set to None those attacks will not be touched. Defaults to dwarves, humans, and elves. + +## stal.armoury.items + +Created by Stalhansch and Sophie Kirschner. + +Attempts to improve the balance and realism of combat. + +#### Arguments: + +* **remove_entity_items:** Determines whether items that would be made unavailable to entities should be removed from those entities or not. If you're also using other mods that make changes to weapons and armour and such it may be desireable to set this flag to False. Otherwise, for best results, the flag should be set to True. Defaults to True + +## umiman.smallthings.engraving + +Created by Umiman, Fieari, and Sophie Kirschner. + +Has this been done before? While I think it's impossible to change any of the inbuilt engraving stuff like, "this is a picture of a dwarf and a tentacle demon. The dwarf is embracing the tentacle demon", it is possible to edit and add to more basic ones such as "this is a picture of a crescent moon". Basically, I added maybe 100 or so new engravings you can potentially see on your floors, walls, studded armour, images, and the like. Keep in mind maybe one or two metagame just a tad but it's funny! I swear! + +## umiman.smallthings.prefstring + +Created by Umiman, Fieari, and Sophie Kirschner. + +This mod simply just adds to the number of prefstrings for everything in the game to a minimum of five each. Prefstrings are the stuff that tell your dwarves what to like about a creature. For example, "He likes large roaches for their ability to disgust". With this mod, you'll see more fleshed out descriptions of the things your dwarves like, as well as more varied ones. With five each, it's pretty rare to see the same two twice. Hopefully I don't have any repeating prefstrings. + +## umiman.smallthings.speech.nofamily + +Created by Umiman and Sophie Kirschner. + +Adds more dialog options to no_family.txt. + +## umiman.smallthings.speech.threats + +Created by Umiman and Sophie Kirschner. + +Awhile back I asked the community to contribute to fill out the threat.txt which is used in adventurer when someone threatens you. I.E: in vanilla, when you face a megabeast or someone who has killed a named creature, they will talk about who they killed and then say, "prepare to die!!!". That's all they said. Boring. This compilation has some of the best threats (around 150 and counting) compiled from that thread and should make killing things too proud of their own achievements a lot more fun. + +## witty.restrictednobles.custom + +Created by Witty and Sophie Kirschner. + +Allows allowing and preventing various species from becoming dwarven nobles. + +#### Arguments: + +* **inclusions:** An iterable containing each species that should be specified as allowed. If any is allowed in this way, any species not specifically allowed will be disallowed. + +* **exclusions:** An iterable containing each species that should be disallowed. All species not disallowed in this way will be able to become dwarven nobles. + +## witty.restrictednobles.standard + +Created by Witty and Sophie Kirschner. + +Witty: This is a pretty simple mod I've been meaning to make for a while. This should restrict all nobles of a given dwarven civ to dwarves and only dwarves. edit: taking into consideration that non-dwarves will be functional fort citizens as of the next version, I've decided to go with another option. The newest addition will now only exclude goblins from dwarven positions, since their current worldgen behavior still makes them the most likely to dominate dwarven nobility. But now the occasional elf or human king will get their fair dues. The dwarf-only "module" will still come packaged. Note this will require a new world to take effect. All raw changes will be indicated by the WM insignia. Sophie: By default, this script will only prevent goblins from becoming nobles. Set the onlydwarves flag to True in order to prevent all other races as well. + +#### Arguments: + +* **onlydwarves:** Defaults to False. If True, only dwarves will be allowed to hold positions in Dwarven forts and civs. If False, only goblins will be prevented from holding those positions. \ No newline at end of file diff --git a/tutorial.md b/docs/tutorial.md similarity index 94% rename from tutorial.md rename to docs/tutorial.md index bf4c5fe..38b9bc9 100644 --- a/tutorial.md +++ b/docs/tutorial.md @@ -35,6 +35,7 @@ For convenient reference, this is an example config.json file: "backup": "E:/Sophie/Desktop/Files/Games/Dwarf Fortress/df_40_24_win/rawbak/", "version": "auto", + "hackversion": "auto", "scripts": [ {"name": "pineapple.deerappear", "args": {"tile": "'d'", "color": [6, 0, 1]}}, @@ -57,6 +58,8 @@ Here is the purpose of each of those attributes: * `output`: Which directory to output the raws to after the scripts have run and modified them. If set to `null` (in json) or `None` (in Python) then the output directory will be the same as the input. * `backup`: Before anything else is done, if it's not `null` or `None`, the input raws will be copied and saved to this directory. * `version`: Specifies the Dwarf Fortress version. For example, `"version": "0.40.24"`. If set to `auto` then PyDwarf will attempt to detect the Dwarf Fortress version automatically. This should succeed as long as either the `input` or `output` directory is somewhere inside Dwarf Fortress's directory. +* `dfhackdir`: The location of a hack/ directory, if any, within the Dwarf Fortress directory +* `hackversion`: The version of DFHack contained within a hack/ directory, if any, within the Dwarf Fortress directory * `scripts`: Lists the scripts that should be run. * `packages`: Lists the Python packages that should be imported. In essence, it specifies for PyDwarf that it should look for scripts inside the `scripts` package, in this case a directory containing an `__init__.py` file. This is an advanced feature and the typical user won't need to worry about this. @@ -68,22 +71,21 @@ Once PyDwarf has been configured, applying the mods specified in its configurati PyDwarf's configuration can also be passed as command line arguments when running manager.py. These are the arguments it accepts, all of which supersede any identically-named options set in `config.json` or `config.py` when specified. -* `-i` or `--input`: Specifies input directory. -* `-o` or `--output`: Specifies output directory. -* `-b` or `--backup`: Specifies backup directory. +* `-i` or `--input`: Specifies raws input directory. +* `-o` or `--output`: Specifies raws output directory. +* `-b` or `--backup`: Specifies raws backup directory. * `-ver` or `--version`: Specifies Dwarf Fortress version. +* `-hver` or `--hackversion`: Specifies DFHack version. * `-s` or `--scripts`: The list of scripts to run. (Only names and namespaces may be specified in this way, not dictionaries.) * `-p` or `--packages`: The list of Python packages to import. -* `-c` or `--config`: Imports configuration from the json file given by the path. +* `-c` or `--config`: Reads configuration from the json file given by the path. Can also refer to a Python file or package, which will be imported and used for configuration. See `config_override.py` for an example. * `-v` or `--verbose`: Sets the logging level for standard output to `DEBUG`. (By default, fully verbose logs are written to the `logs/` directory regardless of this flag.) * `--log`: Specifies the log file path. * `--list`: Lists registered scripts in alphabetical order. * `--meta`: When given names of scripts as arguments, shows each script's metadata in a readable format. When given no arguments, metadata for all registered scripts is displayed. -* `--jscripts`: Alternative to `--scripts` which accepts a json array just like the `scripts` attribute in `config.json`. +* `--jscripts`: More complicated alternative to `--scripts` which accepts a json array just like the `scripts` attribute in `config.json`. * `-h` or `--help`: Shows a summary of each argument's purpose. -The arguments include `-i` or `--input`, `-o` or `--output`, `-b` or `--backup`, `-ver` or `--version`, which behave identically to their `config.json` or `config.py` counterparts. Additional arguments are `-s` or `--scripts`, which is a list of scripts just the same except with support only for strings (not for dicts), and `-c` or `--config`, which can be used to specify a json file to read as configuration. - ![Example gif of running PyDwarf from the command line](http://www.pineapplemachine.com/pydwarf/terminal_example.gif) @@ -133,7 +135,6 @@ Type "help", "copyright", "credits" or "license" for more information. >>> print description [DESCRIPTION:A medium-sized creature undeserving of life.] >>> df.write(path='raw/objects') - >>> quit() client-170:PyDwarf pineapple$ grep 'CREATURE:ELF' raw/objects/creature_standard.txt -A 4 [CREATURE:ELF] @@ -168,4 +169,4 @@ for bear in df.allobj(type='CREATURE', re_id='BEAR_.+'): bear.get('CASTE:FEMALE').add('FLIER') ``` -![Image of a flying female bear](https://github.com/pineapplemachine/PyDwarf/blob/master/images/logo_transparent.png?raw=true) +![Image of a flying female bear](https://github.com/pineapplemachine/PyDwarf/blob/master/images/logo_transparent.png?raw=true) \ No newline at end of file diff --git a/images/favicon.png b/images/favicon.png new file mode 100644 index 0000000..5960997 Binary files /dev/null and b/images/favicon.png differ diff --git a/manager.py b/manager.py index 259a451..3054b4b 100644 --- a/manager.py +++ b/manager.py @@ -1,25 +1,87 @@ +#!/usr/bin/env python + +__author__ = 'Sophie Kirschner' +__license__ = 'zlib/libpng' +__email__ = 'sophiek@pineapplemachine.com' +__version__ = '1.0.2' + + + import re import os +import shutil import json import argparse import importlib import pydwarf import raws -from config import config -__version__ = '1.0.1' +jsonconfigpath = 'config.json' -jsonconfigpath = 'config.json' +# Actually run the program +def __main__(args=None): + conf = getconf(args) + pydwarf.log.debug('Proceeding with configuration: %s.' % conf) + + # Report versions + pydwarf.log.info('Running PyDwarf manager version %s.' % __version__) + pydwarf.log.debug('With pydwarf version %s.' % pydwarf.__version__) + pydwarf.log.debug('With raws version %s.' % raws.__version__) + pydwarf.log.debug('With Dwarf Fortress version %s.' % conf.version) + pydwarf.log.debug('With DFHack version %s.' % conf.hackversion) + + # Handle flags that completely change behavior + specialtext = None + + if args.list: + items = pydwarf.urist.list() + specialtext = '\n'.join(items) + elif args.meta is not None: + specialtext = pydwarf.urist.doclist(args.meta, format=args.metaformat) + + if specialtext is not None: + pydwarf.log.info('\n\n%s\n' % specialtext) + if args.writedoc: + with open(args.writedoc, 'wb') as writedoc: writedoc.write(specialtext) + exit(0) + + # Verify that input directory exists + if not os.path.exists(conf.input): + pydwarf.log.error('Specified raws directory %s does not exist.' % conf.input) + exit(1) + + # Create a new session + pydwarf.log.info('Configuring session using raws input directory %s.' % conf.input) + session = pydwarf.session(raws, conf) + + # Make backup + if conf.backup is not None: + pydwarf.log.info('Backing up raws to directory %s.' % conf.backup) + session.backup() + else: + pydwarf.log.warning('Proceeding without backing up raws.') + + # Run each script + pydwarf.log.info('Running scripts.') + session.handleall() + + # Write the output + outputdir = conf.output if conf.output else conf.input + pydwarf.log.info('Writing new raws to directory %s.' % outputdir) + session.write(outputdir) + + # All done! + pydwarf.log.info('All done!') def getconf(args=None): # Load initial config from json file - conf = config() + conf = pydwarf.config() if os.path.isfile(jsonconfigpath): conf.json(jsonconfigpath) # Default name of configuration override package @@ -52,93 +114,32 @@ def getconf(args=None): if overrideexception: pydwarf.log.error('Failed to apply configuration from %s package.\n%s' % (overridename, overrideexception)) - # Setup version (Handle 'auto') - conf.setupversion() - - # Import packages - conf.setuppackages() + # Handle things like automatic version detection, package importing + conf.setup() # All done! return conf -# Actually run the program -def __main__(args=None): - conf = getconf(args) - pydwarf.log.debug('Proceeding with configuration: %s.' % conf) - - # Report versions - pydwarf.log.info('Running PyDwarf manager version %s.' % __version__) - pydwarf.log.debug('With PyDwarf version %s.' % pydwarf.__version__) - pydwarf.log.debug('With raws version %s.' % raws.__version__) - - # Handle flags that completely change behavior - if args.list: - pydwarf.urist.list() - exit(0) - elif args.meta is not None: - pydwarf.urist.doclist(args.meta) - exit(0) - - # Verify that input directory exists - if not os.path.exists(conf.input): - pydwarf.log.error('Specified raws directory %s does not exist.' % conf.input) - exit(1) - - # Make backup - if conf.backup is not None: - pydwarf.log.info('Backing up raws to %s.' % conf.backup) - try: - raws.copytree(conf.input, conf.backup) - except: - pydwarf.log.error('Failed to create backup.') - exit(1) - else: - pydwarf.log.warning('Proceeding without backing up raws.') - - # Read input raws - pydwarf.log.info('Reading raws from input directory %s.' % conf.input) - pydwarf.urist.session.dfraws = raws.dir(path=conf.input, log=pydwarf.log) - - # Run each script - pydwarf.log.info('Running scripts.') - pydwarf.urist.session.handleall(conf.scripts) - - # Get the output directory, remove old raws if present - outputdir = conf.output if conf.output else conf.input - if os.path.exists(outputdir): - pydwarf.log.info('Removing obsolete raws from %s.' % outputdir) - for removefile in [os.path.join(outputdir, f) for f in os.listdir(outputdir)]: - pydwarf.log.debug('Removing file %s.' % removefile) - if removefile.endswith('.txt'): os.remove(removefile) - else: - pydwarf.log.info('Creating raws output directory %s.' % outputdir) - os.makedirs(outputdir) - - # Write the output - pydwarf.log.info('Writing changes to raws to %s.' % outputdir) - pydwarf.urist.session.dfraws.write(outputdir, pydwarf.log) - - # All done! - pydwarf.log.info('All done!') - - - def parseargs(): parser = argparse.ArgumentParser() parser.add_argument('-ver', '--version', help='indicate Dwarf Fortress version', type=str) parser.add_argument('-i', '--input', help='raws input directory', type=str) parser.add_argument('-o', '--output', help='raws output directory', type=str) parser.add_argument('-b', '--backup', help='raws backup directory', type=str) + parser.add_argument('-t', '--paths', help='which paths relative to input to store in memory and allow access to', nargs='+', type=str) parser.add_argument('-s', '--scripts', help='run scripts by name or namespace', nargs='+', type=str) parser.add_argument('-p', '--packages', help='import packages containing PyDwarf scripts', nargs='+', type=str) parser.add_argument('-c', '--config', help='run with json config file if the extension is json, otherwise treat as a Python package, import, and override settings using export dict', type=str) parser.add_argument('-v', '--verbose', help='set stdout logging level to DEBUG', action='store_true') + parser.add_argument('-hver', '--hackversion', help='indicate DFHack version', type=str) parser.add_argument('--log', help='output log file to path', type=str) parser.add_argument('--list', help='list available scripts', action='store_true') parser.add_argument('--jscripts', help='specify scripts given a json array', type=str) parser.add_argument('--meta', help='show metadata for scripts', nargs='*', type=str) + parser.add_argument('--metaformat', help='how to format shown metadata', type=str) + parser.add_argument('--writedoc', help='write data given by --list or --meta to a file path in addition to the log', type=str) args = parser.parse_args() if args.jscripts is not None: args.scripts = json.loads(args.jscripts) if args.scripts is None else (args.scripts + json.loads(args.jscripts)) diff --git a/pydwarf/__init__.py b/pydwarf/__init__.py index d47658b..12b81e8 100644 --- a/pydwarf/__init__.py +++ b/pydwarf/__init__.py @@ -1,5 +1,31 @@ +#!/usr/bin/env python + +__author__ = 'Sophie Kirschner' +__license__ = 'zlib/libpng' +__email__ = 'sophiek@pineapplemachine.com' +__version__ = '1.0.2' + + + +''' + +The pydwarf package acts as a layer of abstraction over the raws package, providing functionality for mod management and application. + +pydwarf.log: A shared logging object. +pydwarf.version: Utilities pertinent to handling Dwarf Fortress versions. +pydwarf.response: PyDwarf expects plugins to return pydwarf.response objects. +pydwarf.urist: A combined decorator and global repository for PyDwarf plugins. +pydwarf.session: Ideally for use by a mod manager, abstracts the handling and execution of PyDwarf plugins. +pydwarf.config: Provides an object to simplify config loading and application. +pydwarf.helpers: Contains some miscellaneous utility functions. + +''' + + + +from log import * from version import * from response import * from urist import * - -__version__ = '1.0.1' +from config import * +from helpers import * diff --git a/pydwarf/config.py b/pydwarf/config.py new file mode 100644 index 0000000..55d2259 --- /dev/null +++ b/pydwarf/config.py @@ -0,0 +1,172 @@ +import sys +import os +import logging +import json +import importlib +from datetime import datetime + +from log import log +from version import detectversion +from urist import urist, session +from helpers import findfile + + + +# Used in some file paths and such +datetimeformat = '%Y.%m.%d.%H.%M.%S' +timestamp = datetime.now().strftime(datetimeformat) + +auto_paths = [ + 'gamelog.txt', 'errorlog.txt', + 'stderr.log', 'stdout.log', + 'raw/objects', 'raw/graphics', + 'data/art', 'data/init', 'data/speech', + 'dfhack.init', 'dfhack.init-example', 'dfhack.history', + 'hack/lua', 'hack/plugins', 'hack/raw', 'hack/ruby', 'hack/scripts', + 'stonesense', +] + + + +class config: + def __init__(self, version=None, paths=None, hackversion=None, input=None, output=None, backup=None, scripts=[], packages=[], verbose=False, log='logs/%s.txt' % timestamp): + self.version = version # Dwarf Fortress version, for handling script compatibility metadata + self.hackversion = hackversion # DFHack version + self.input = input # Raws are loaded from this input directory + self.output = output # Raws are written to this output directory + self.backup = backup # Raws are backed up to this directory before any changes are made + self.paths = paths # Files are only handled in these paths, relative to input + self.scripts = scripts # These scripts are run in the order that they appear + self.packages = packages # These packages are imported (probably because they contain PyDwarf scripts) + self.verbose = verbose # Log DEBUG messages to stdout if True, otherwise only INFO and above + self.log = log # Log file goes here + + def __str__(self): + return str(self.__dict__) + def __repr__(self): + return self.__str__() + + def __getitem__(self, attr): + return self.__dict__[attr] + def __setitem__(self, attr, value): + self.__dict__[attr] = value + + def __iter__(self): + return iter(self.__dict__) + def iteritems(self): + return self.__dict__.iteritems() + + def __add__(self, other): + return config.concat(self, other) + def __radd__(self, other): + return config.concat(other, self) + def __iadd__(self, item): + self.apply(item) + return self + + def __and__(self, other): + return config.intersect(self, other) + + def json(self, path, *args, **kwargs): + with open(path, 'rb') as jsonfile: return self.apply(json.load(jsonfile), *args, **kwargs) + + def apply(self, data, applynone=False): + if data: + for key, value in data.iteritems(): + if applynone or value is not None: self.__dict__[key] = value + return self + + def copy(self): + copy = config() + for key, value in self: copy[key] = value + return copy + + @staticmethod + def concat(*configs): + result = config() + for conf in configs: result.apply(conf) + return result + + @staticmethod + def intersect(*configs): + result = config() + first = configs[0] + for attr, value in first.iteritems(): + for conf in configs: + if (conf is not first) and (attr not in conf or conf[attr] != value): break + else: + result[attr] = value + return result + + def setup(self, logger=False): + # Set up the pydwarf logger + if logger: self.setuplogger() + # Handle paths == 'auto' or ['auto'] + self.setuppaths() + # Handle version == 'auto' + self.setupversion() + # Handle hackversion == 'auto' + self.setuphackversion() + # Import packages + self.setuppackages() + + def setuplogger(self): + # Set up the logger (And it should be done first thing!) + log.setLevel(logging.DEBUG) + # Handler for console output + stdouthandler = logging.StreamHandler(sys.stdout) + stdouthandler.setLevel(logging.DEBUG if self.verbose else logging.INFO) + stdouthandler.setFormatter(logging.Formatter('%(asctime)s: %(levelname)s: %(message)s', datetimeformat)) + log.addHandler(stdouthandler) + # Handler for log file output + if self.log: + logdir = os.path.dirname(self.log) + if not os.path.exists(logdir): os.makedirs(logdir) + logfilehandler = logging.FileHandler(self.log) + logfilehandler.setLevel(logging.DEBUG) + logfilehandler.setFormatter(logging.Formatter('%(asctime)s: %(filename)s[%(lineno)s]: %(levelname)s: %(message)s', datetimeformat)) + log.addHandler(logfilehandler) + + def setuppackages(self): + self.importedpackages = [importlib.import_module(package) for package in self.packages] + + def setuppaths(self): + if self.paths == 'auto' or self.paths == ['auto'] or self.paths == ('auto',): + self.paths = auto_paths + + def setupversion(self): + # Handle automatic version detection + if self.version == 'auto': + log.debug('Attempting to automatically detect Dwarf Fortress version.') + self.version = detectversion(paths=(self.input, self.output)) + if self.version is None: + log.error('Unable to detect Dwarf Fortress version.') + else: + log.debug('Detected Dwarf Fortress version %s.' % self.version) + elif self.version is None: + log.warning('No Dwarf Fortress version was specified. Scripts will be run regardless of their indicated compatibility.') + else: + log.info('Managing Dwarf Fortress version %s.' % self.version) + + def setuphackversion(self): + if self.hackversion == 'auto': + log.debug('Attempting to automatically detect DFHack version.') + + dfhackdir = findfile(name='hack', paths=(self.input, self.output)) + if dfhackdir is None: + log.error('Unable to detect DFHack directory.') + return + else: + log.debug('Detected DFHack directory at %s.' % dfhackdir) + + newspath = os.path.join(dfhackdir, 'NEWS') + if os.path.isfile(newspath): + with open(newspath, 'rb') as news: self.hackversion = news.readline().strip() + + if self.hackversion is None: + log.error('Unable to detect DFHack version.') + else: + log.debug('Detected DFHack version %s.' % self.hackversion) + + elif self.hackversion is None: + log.warning('No DFHack version was specified.') diff --git a/pydwarf/helpers.py b/pydwarf/helpers.py new file mode 100644 index 0000000..1b3afb5 --- /dev/null +++ b/pydwarf/helpers.py @@ -0,0 +1,25 @@ +import os +from log import log + + + +def rel(base, *parts): + return os.path.join(os.path.dirname(base) if os.path.isfile(base) else base, *parts) + + + +def findfile(name, paths, recursion=6): + log.debug('Looking for file %s.' % name) + for path in paths: + if path is not None: + currentpath = path + for i in xrange(0, recursion): + if os.path.isdir(currentpath): + log.debug('Checking path %s for file %s.' % (currentpath, name)) + if name in os.listdir(currentpath): + return os.path.join(currentpath, name) + else: + currentpath = os.path.dirname(currentpath) + else: + break + return None diff --git a/pydwarf/log.py b/pydwarf/log.py new file mode 100644 index 0000000..77c2a81 --- /dev/null +++ b/pydwarf/log.py @@ -0,0 +1,3 @@ +# Super simple: just make a default, shared logger object +import logging +log = logging.getLogger() diff --git a/pydwarf/response.py b/pydwarf/response.py index 9967816..18b31dd 100644 --- a/pydwarf/response.py +++ b/pydwarf/response.py @@ -6,6 +6,9 @@ def __init__(self, success, status): def __str__(self): return '%s: %s' % ('SUCCESS' if self.success else 'FAILURE', self.status if self.status else ('Ran %ssuccessfully.' % ('' if self.success else 'un'))) + def __nonzero__(self): + return self.success + @staticmethod def success(status=None): return response(True, status) diff --git a/pydwarf/urist.py b/pydwarf/urist.py index 98bb937..b48bffe 100644 --- a/pydwarf/urist.py +++ b/pydwarf/urist.py @@ -1,22 +1,32 @@ -import logging +import os +import shutil import textwrap -import version as versionutils - - -# Make a default logger object -log = logging.getLogger() +import version as versionutils +from log import log +import uristdoc class session: - def __init__(self, dfraws=None, dfversion=None): - self.dfraws = dfraws - self.dfversion = dfversion + def __init__(self, raws=None, conf=None): + self.dfraws = None + self.dfversion = None + self.hackversion = None + self.conf = None + self.raws = None self.successes = [] self.failures = [] self.noresponse = [] + if raws is not None and conf is not None: self.configure(raws, conf) + def configure(self, raws, conf): + self.raws = raws + self.conf = conf + self.dfraws = raws.dir(root=conf.input, dest=conf.output, paths=conf.paths, version=conf.version, log=log) + self.dfversion = conf.version + self.hackversion = conf.hackversion + def successful(self, info): return self.inlist(info, self.successes) def failed(self, info): @@ -38,23 +48,24 @@ def eval(self, func, args=None): name = uristinstance.getname() else: name = func.__name__ + # Actually execute the script log.info('Running script %s%s.' % (name, ('with args %s' % args) if args else '')) try: - # Call the function - response = func(self.dfraws, **args) if args else func(self.dfraws) - if response: + response = func(self.dfraws, **args) if args else func(self.dfraws) # Call the function + if response is not None: # Handle success/failure response log.info(str(response)) (self.successes if response.success else self.failures).append(uristinstance if uristinstance else func) else: log.error('Received no response from script %s.' % name) self.noresponse.append(uristinstance if uristinstance else func) + except Exception: log.exception('Unhandled exception while running script %s.' % name) return False + else: - log.info('Finished running script %s.' % name) return True def funcs(self, info): @@ -82,11 +93,28 @@ def handle(self, info): else: log.error('Found no scripts matching %s.' % info) - def handleall(self, infos): + def handleall(self, infos=None): + if infos is None and self.conf is not None: infos = self.conf.scripts if infos and len(infos): for info in infos: self.handle(info) else: log.error('No scripts to run.') + + def write(self, dest=None, *args, **kwargs): + self.dfraws.clean(dest=dest) + self.dfraws.write(dest=dest, *args, **kwargs) + + def backup(self, dest=None): + if dest is None: dest = self.conf.backup + if dest: + for path in self.conf.paths: + srcpath = os.path.join(self.conf.input, path) + destpath = os.path.join(dest, path) + if not os.path.isdir(os.path.dirname(destpath)): os.makedirs(os.path.dirname(destpath)) + if os.path.isfile(srcpath): + shutil.copy2(srcpath, destpath) + else: + self.raws.copytree(srcpath, destpath) @@ -122,8 +150,6 @@ class urist: # Track registered functions registered = {} - # Track data about which scripts have run successfully, etc. - session = session() # Decorator handling def __init__(self, **kwargs): @@ -149,8 +175,8 @@ def __hash__(self): def getname(self): return '.'.join((self.namespace, self.name)) if self.namespace else self.name - def meta(self, key): - return self.metadata.get(key) + def meta(self, key, default=None): + return self.metadata.get(key, default) def matches(self, match): return all([self.meta(i) == j for i, j in match.iteritems()]) if match else True @@ -223,7 +249,7 @@ def get(name, version=None, match=None, session=None): return urist.cullcandidates( version = version, match = match, - session = session if session is not None else urist.session, + session = session, candidates = urist.getregistered(*urist.splitname(name)) ) @@ -278,9 +304,12 @@ def cullcandidates_compatibility(version, candidates): @staticmethod def cullcandidates_dependency(session, candidates): - newcand, culled = [], [] - for cand in candidates: (newcand if cand.depsatisfied(session) else culled).append(cand) - return newcand, culled + if session: + newcand, culled = [], [] + for cand in candidates: (newcand if cand.depsatisfied(session) else culled).append(cand) + return newcand, culled + else: + return candidates, [] @staticmethod def cullcandidates_duplicates(candidates): @@ -316,7 +345,6 @@ def splitname(name): @staticmethod def list(): - log.info('Listing registered scripts.') names = {} total = 0 for uristlist in urist.registered.itervalues(): @@ -325,69 +353,41 @@ def list(): if uname not in names: names[uname] = [] names[uname].append(uristinstance) total += 1 - log.info('Found %d registered scripts in total.' % total) - for name, uristlist in sorted(names.items()): - log.info('Found %d script%s named %s.' % (len(uristlist), 's' if len(uristlist) > 1 else '', name)) + # log.info('Found %d registered scripts in total.' % total) + # for name, uristlist in sorted(names.items()): + # log.info('Found %d script%s named %s.' % (len(uristlist), 's' if len(uristlist) > 1 else '', name)) + return sorted(names.keys()) - def doc(self): + def doc(self, format=None): '''Make a pretty metadata string.''' - doc = '' - - # Utility function - def normalize(string): return ' '.join([l.strip() for l in str(string).split('\n')]) - - # Title - author = self.meta('author') - version = self.meta('version') - if author and not isinstance(author, basestring): author = ', '.join(author) - versionstr = (' %s' % version) if version else '' - authorstr = (' by %s' % author) if author else '' - doc += 'Script:\n %s%s%s.' % (self.getname(), versionstr, authorstr) - - # Description - desc = self.meta('description') - if desc: - doc += '\n\nDescription:\n%s' % textwrap.fill(' %s' % normalize(desc)) - - # Arguments - args = self.meta('arguments') - if args: - doc += '\n\nArguments:' - for argname, arginfo in args.iteritems(): - doc += '\n%s' % textwrap.fill(' %s: %s' % (argname, normalize(arginfo))) - - # Dependencies - deps = self.meta('dependency') - if deps: - if not isinstance(deps, basestring): deps = ', '.join(deps) - doc += '\nDepends on:\n%s' % textwrap.fill(' %s' % deps) - - # Compatibility - compat = self.meta('compatibility') - if compat: - doc += '\n\nDF version compatibility regex:\n %s' % str(compat) - - # Everything else - othermeta = [] - for key, value in self.metadata.iteritems(): - if key not in ('name', 'namespace', 'author', 'version', 'description', 'arguments', 'dependency', 'compatibility'): othermeta.append((key, value)) - if len(othermeta): - doc += '\n\nOther metadata:' - for key, value in othermeta: - doc += '\n%s' % textwrap.fill(' %s: %s' % (key, normalize(value))) + template = uristdoc.template.format.get(format if format else 'txt') + if template is None: raise KeyError('Failed to create documentation string because the format %s was unrecognized.' % format) - # All done! - return doc + handled_metadata_keys = ('name', 'namespace', 'author', 'version', 'description', 'arguments', 'dependency', 'compatibility') + return template.full( + name = self.getname(), + version = self.meta('version'), + author = self.meta('author'), + description = self.meta('description'), + compatibility = self.meta('compatibility'), + dependencies = self.meta('dependency'), + arguments = self.meta('arguments'), + metadata = {key: value for key, value in self.metadata.iteritems() if key not in handled_metadata_keys} + ) + @staticmethod - def doclist(names=[]): - log.info('Showing metadata for scripts.') + def doclist(names=[], delimiter='\n\n', format=None): urists = [] if len(names): for name in names: urists += urist.getregistered(*urist.splitname(name)) else: urists = urist.allregistered() - for uristinstance in urists: - log.info('\n\n%s\n' % uristinstance.doc()) - \ No newline at end of file + items = sorted(ur.doc(format=format) for ur in urists) + template = uristdoc.template.format.get(format if format else 'txt') + if items and template: + text = template.concat(items) + else: + text = delimiter.join(items) + return text diff --git a/pydwarf/uristdoc.py b/pydwarf/uristdoc.py new file mode 100644 index 0000000..c1bfe53 --- /dev/null +++ b/pydwarf/uristdoc.py @@ -0,0 +1,192 @@ +import os +import textwrap + + +def __main__(): + template.format['txt'] = txttemplate() + template.format['md'] = mdtemplate() + template.format['html'] = htmltemplate() + + + +class template: + '''Internal: Helpful class for handling different formats used by the urist.doc method.''' + + format = {} + + def preprocess(self, item): + if isinstance(item, basestring): + return self.preprocesstext(item) + elif isinstance(item, list) or isinstance(item, tuple): + return [self.preprocess(i) for i in item] + elif isinstance(item, dict): + return {self.preprocess(key): self.preprocess(value) for key, value in item.iteritems()} + else: + return item + def preprocesstext(self, text): + return text + + def concat(self, items): + return '\n\n'.join(items) + + def join(self, items, delimiter=',', natural=False): + if not items: + return None + elif isinstance(items, basestring): + return items + elif len(items) == 1: + return items[0] + elif natural: + if len(items) == 2: + return ' and '.join(str(item) for item in items) + else: + return '%s%s and %s' % ( + ('%s ' % delimiter).join(str(item) for item in items[:-1]), + delimiter, + items[-1] + ) + else: + return delimiter.join(str(item) for item in items) + def fmt(self, string, format): + return string % format if string and format else None + + def header(self, **kwargs): + return None + def dependencies(self, **kwargs): + return None + def arguments(self, **kwargs): + return None + def metadata(self, **kwargs): + return None + + def norm(self, text): + return ' '.join([line.strip() for line in str(text).split('\n')]) + def wrap(self, text): + return textwrap.fill(text) if self.wraptext else text + + def full(self, delimiter=None, dependencies=None, arguments=None, metadata=None, **kwargs): + if delimiter is None: delimiter = self.delimiter + dependencies = (dependencies,) if dependencies and isinstance(dependencies, basestring) else dependencies + + kwargs = {key: self.preprocess(value) for key, value in kwargs.iteritems()} + dependencies = self.preprocess(dependencies) + arguments = self.preprocess(arguments) + metadata = self.preprocess(metadata) + + body = delimiter.join(item for item in ( + self.header(**kwargs), + self.dependencies(dependencies) if dependencies else None, + self.arguments(arguments) if arguments else None, + self.metadata(metadata) if metadata else None + ) if item) + return '\n'.join(self.wrap(line) for line in body.split('\n')) if self.wraptext else body + + + +class txttemplate(template): + def __init__(self): + self.delimiter = '\n\n' + self.wraptext = True + + def header(self, name=None, version=None, author=None, description=None, **kwargs): + headeritems = ( + name, + self.fmt('version %s', version), + self.fmt('by %s', self.join(author)) + ) + header = 'Script %s:' % ' '.join(item for item in headeritems if item) + return '%s\n\n%s' % (header, self.norm(description)) if description else header + + def dependencies(self, dependencies): + return 'Dependencies:\n%s' % '\n'.join(dependencies) + def arguments(self, arguments): + return 'Arguments:\n%s' % '\n'.join((' %s: %s' % (key, self.norm(value)) for key, value in arguments.iteritems())) + def metadata(self, metadata): + return 'Metadata:\n%s' % '\n'.join((' %s: %s' % (key, self.norm(value)) for key, value in metadata.iteritems())) + + + +class mdtemplate(template): + def __init__(self): + self.delimiter = '\n\n' + self.wraptext = False + + def preprocesstext(self, text): + return text.replace('*', '\*').replace('#', '\#') + + def concat(self, items): + return '''# Scripts\n\n%s''' % '\n\n'.join(items) + + def header(self, name=None, version=None, author=None, description=None, **kwargs): + headeritems = ( + self.fmt('## %s', name), + #self.fmt('Version %s', version), + self.fmt('Created by %s.', self.join(author, natural=True)), + self.norm(description) + ) + return '\n\n'.join(item for item in headeritems if item) + + def dependencies(self, dependencies): + return '#### Dependencies:\n\n%s' % ' \n'.join('* %s' % dep for dep in dependencies) + def arguments(self, arguments): + return '#### Arguments:\n\n%s' % '\n\n'.join(('* **%s:** %s' % (key, self.norm(value)) for key, value in arguments.iteritems())) + def metadata(self, metadata): + return '#### Metadata:\n\n%s' % '\n\n'.join(('* **%s:** %s' % (key, self.norm(value)) for key, value in metadata.iteritems())) + + + +class htmltemplate(template): + def __init__(self): + self.delimiter = '\n' + self.wraptext = False + + def preprocesstext(self, text): + return text.replace('&', '&').replace('<', '<').replace('>', '>') + + def concat(self, items): + return ''' + + + + Scripts + + + + +

Scripts

+ %s + + + ''' % '\n\n'.join('
%s
' % item for item in items) + + def header(self, name=None, version=None, author=None, description=None, **kwargs): + headeritems = ( + self.fmt('

%s

', name), + #self.fmt('

Version %s

', version), + self.fmt('

Created by %s.

', self.join(author, natural=True)), + self.norm(description) + ) + return '\n'.join(item for item in headeritems if item) + + def dependencies(self, dependencies): + return '

Dependencies

\n' % '\n'.join('
  • %s
  • ' % dep for dep in dependencies) + def arguments(self, arguments): + return '

    Arguments

    \n' % '\n'.join(('
  • %s: %s
  • ' % (key, self.norm(value)) for key, value in arguments.iteritems())) + def metadata(self, metadata): + return '

    Metadata

    \n' % '\n'.join(('
  • %s: %s
  • ' % (key, self.norm(value)) for key, value in metadata.iteritems())) + + + +__main__() diff --git a/pydwarf/version.py b/pydwarf/version.py index aa2b592..f0e9fde 100644 --- a/pydwarf/version.py +++ b/pydwarf/version.py @@ -1,6 +1,11 @@ import os import re +from log import log +from helpers import findfile + + + # Can be expected to match all past and future 0.40.* releases. (Time of writing is 21 May 15, the most recent version is 0.40.24.) df_0_40 = '(0\.40\.\d{2,}[abcdefg]?)' # Matches all DF 0.34.* releases @@ -22,6 +27,8 @@ # Matches all DF 0.27, 0.23, 0.22, and 0.21 releases df_0_2x = '|'.join((df_0_21, df_0_22, df_0_23, df_0_27, df_0_28)) + + # Generates a regex which should properly match from, until, and each version in-between. # For example: pydwarf_range('0.40.14', '0.40.24') def df_revision_range(prettymin=None, prettymax=None, major=None, minor=None, minrevision=None, maxrevision=None): @@ -35,6 +42,8 @@ def df_revision_range(prettymin=None, prettymax=None, major=None, minor=None, mi maxrevision = parts[2] if len(parts) > 2 else '0' return '%s\.%s\.(%s)' % (major, minor, '|'.join([str(r) for r in range(int(minrevision), int(maxrevision)+1)])) + + # Given a version and a compatibility regex, determine compatibility def compatible(compatibility, version): if isinstance(compatibility, basestring): @@ -42,25 +51,14 @@ def compatible(compatibility, version): else: return any([re.match(item, version) for item in compatibility]) -def detectversion(paths, recursion=8, log=None): + + +def detectversion(*args, **kwargs): # Given a list of directories that may be inside a DF directory, e.g. raws input or output, look for release notes.txt and get the version from that - if log: log.debug('Attempting to detect Dwarf Fortress version given paths %s.' % [str(p) for p in paths]) - for path in paths: - if path is not None: - currentpath = path - for i in xrange(0, recursion): - if os.path.isdir(currentpath): - if log: log.debug('Checking path %s for release notes.' % currentpath) - if 'release notes.txt' in os.listdir(currentpath): - version = versionfromreleasenotes(os.path.join(currentpath, 'release notes.txt'), log) - if version: return version - else: - currentpath = os.path.dirname(currentpath) - else: - break - return None + path = findfile(name='release notes.txt', *args, **kwargs) + return versionfromreleasenotes(path) -def versionfromreleasenotes(path, log=None): +def versionfromreleasenotes(path): with open(path, 'rb') as releasenotes: for line in releasenotes.readlines(): if line.startswith('Release notes for'): return line.split()[3] diff --git a/raws/__init__.py b/raws/__init__.py index c44fc92..f640bf8 100644 --- a/raws/__init__.py +++ b/raws/__init__.py @@ -1,16 +1,56 @@ +#!/usr/bin/env python + +__author__ = 'Sophie Kirschner' +__license__ = 'zlib/libpng' +__email__ = 'sophiek@pineapplemachine.com' +__version__ = '1.0.2' + + + +''' + +The raws package provides querying and modification functionality for Dwarf Fortress raws. + +raws.dir: An entire directory of raws files, stored as a dictionary of files. +raws.token: A single token within a raws file, for example [CREATURE:DWARF] or [INORGANIC:IRON]. +raws.tokenlist: Extends Python's inbuilt list class with additional, specialized functionality. +raws.queryable: Many raws classes extend this class, which provides token querying functionality. +raws.queryableobj: An extension of the queryable class which adds methods optimized for finding object tokens, like the [CREATURE:DWARF] token which is underneath [OBJECT:CREATURE] in the creature_standard file. +raws.tokenfilter: Used by queryable objects' query method to find tokens meeting specific conditions. +raws.boolfilter: Can be used in place of a tokenfilter for operations like Filter A OR Filter B. +raws.color: Contains a convenience class and objects for dealing with colors in the DF raws. +raws.copytree: A general utility method for copying an entire directory and its contents from one location to another. +raws.objecs: Contains information and helper functions for knowing which object types belong to which headers, such as how [BUILDING_WORKSHOP:ID] belongs to [OBJECT:BUILDING]. + +raws.rawfile: A single raws file, stored as a linked list. +raws.reffile: A file stored as a reference to a source file. +raws.binfile: A file stored in a string, as its binary content. +raws.basefile: A base class which other file types inherit from. + +raws.filter: A convenience alias for raws.tokenfilter. +raws.parse: A convenience alias for raws.token.parse, which accepts an input string and parses it into a tokenlist. +raws.parseone: A convenience alias for raws.token.parseone, which acts like raws.token.parse but expects a single token instead of a list of them. + +''' + + + +# TODO: rename classes internally to reflect what they're exported as here e.g. rawsbinfile -> binfile from filters import rawstokenfilter as tokenfilter from filters import rawsboolfilter as boolfilter from queryable import rawsqueryable as queryable +from queryable import rawsqueryableobj as queryableobj from queryable import rawstokenlist as tokenlist from token import rawstoken as token -from file import rawsfile as file +from file import rawsbasefile as basefile +from file import rawsreffile as reffile +from file import rawsbinfile as binfile +from file import rawsfile as rawfile from dir import rawsdir as dir -from dir import copytree +from copytree import copytree +import objects import color filter = tokenfilter - parse = token.parse parseone = token.parseone - -__version__ = '1.0.1' diff --git a/raws/copytree.py b/raws/copytree.py new file mode 100644 index 0000000..159f6b7 --- /dev/null +++ b/raws/copytree.py @@ -0,0 +1,15 @@ +import os +import shutil + +# credit belongs to http://stackoverflow.com/a/13814557/3478907 +def copytree(src, dst, symlinks=False, ignore=None): + if not os.path.exists(dst): + os.makedirs(dst) + for item in os.listdir(src): + s = os.path.join(src, item) + d = os.path.join(dst, item) + if os.path.isdir(s): + copytree(s, d, symlinks, ignore) + else: + if not os.path.exists(d) or os.stat(src).st_mtime - os.stat(dst).st_mtime > 1: + shutil.copy2(s, d) diff --git a/raws/dir.py b/raws/dir.py index ef81bf3..13a3714 100644 --- a/raws/dir.py +++ b/raws/dir.py @@ -1,107 +1,289 @@ import os import shutil -from queryable import rawsqueryable_obj, rawstokenlist -from file import rawsfile +from copytree import copytree +from queryable import rawsqueryable, rawsqueryableobj, rawstokenlist +from file import rawsbasefile, rawsfile, rawsbinfile, rawsreffile -class rawsdir(rawsqueryable_obj): + +class rawsdir(rawsqueryableobj): '''Represents as a whole all the raws contained within a directory.''' - def __init__(self, *args, **kwargs): + def __init__(self, root=None, dest=None, paths=None, version=None, log=None, **kwargs): '''Constructor for rawsdir object.''' - self.files = {} - self.otherfiles = [] - if len(args) or len(kwargs): self.read(*args, **kwargs) + self.filenames = {} + self.root = root # Root input directory + self.dest = dest # Root output directory + self.paths = paths # Only worry about these file paths in input/output directories + self.version = version + self.log = log + if root: self.read(**kwargs) + + def __str__(self): + return '\n'.join(['%s %s' % (file.kind, str(file)) for file in self.files.itervalues()]) - def getfile(self, filename, create=None): + def __enter__(self): + return self + def __exit__(self, type, value, traceback): + if traceback is None and self.path is not None: self.write(path=self.path) + + def __getitem__(self, name): + return self.getfile(name) + + def __setitem__(self, name, content): + if isinstance(content, rawsbasefile): + if content.dir: content = content.copy() + content.setpath(name) + self.add(file=content, replace=True) + elif isinstance(content, rawsqueryable): + self.add(file=name, replace=True, tokens=content.tokens()) + elif isinstance(content, basestring): + self.add(file=name, replace=True, content=content) + else: + self.add(file=name, tokens=content) + + def __contains__(self, item): + if isinstance(item, rawsbasefile): + return item in self.files.itervalues() + else: + return str(item) in self.files or str(item) in self.filenames + + def getfile(self, name, create=None, conflicts=False): '''Gets the file with a given name. If no file by that name is found, None is returned instead. If creature is set to something other than None, the behavior when no file by some name exists is altered: A new file is created and associated with that name, and then its add method is called using the value for create as its argument.''' - rfile = self.files.get(filename) - if create is not None and rfile is None: - rfile = self.addfile(filename) - rfile.add(create) - return rfile + if isinstance(name, rawsbasefile): + return name if name in self.files.itervalues() else None - def addfile(self, filename=None, rfile=None, path=None): - if path is not None: - return self.addpath(path) + file = self.files.get(name) + if file is None: + file = self.filenames.get(name) + if file is not None: + if len(file) == 1: + file = file[0] + else: + if not conflicts: raise ValueError('Failed to retrieve file from dir because the name found no exact matches, and because multiple files were found with that name.') + return file + + if create is not None and file is None: + file = self.add(name) + file.add(create) + return file + + def iterfiles(self, *args, **kwargs): + return self.files.itervalues(*args, **kwargs) + + def add(self, auto=None, **kwargs): + if auto is not None: + return self.addbyauto(auto, **kwargs) + elif 'file' in kwargs: + return self.addbyfile(**kwargs) + elif 'dir' in kwargs: + return self.addbydir(**kwargs) + elif 'tokens' in kwargs: + return self.addbytokens(**kwargs) + elif 'content' in kwargs: + return self.addbybincontent(**kwargs) + elif 'name' in kwargs: + return self.addbyname(**kwargs) + elif 'path' in kwargs: + path = kwargs['path'] + if os.path.isfile(path): + return self.addbyfilepath(**kwargs) + elif os.path.isdir(path): + return self.addbydirpath(**kwargs) + else: + raise ValueError('Failed to add file because the path %s refers to neither a file nor a directory.' % path) + elif 'filepath' in kwargs: + kwargs['path'] = kwargs['filepath'] + del kwargs['filepath'] + return self.addbyfilepath(**kwargs) + elif 'dirpath' in kwargs: + kwargs['path'] = kwargs['dirpath'] + del kwargs['dirpath'] + return self.addbydirpath(**kwargs) else: - if rfile and not filename: filename = rfile.header - if filename in self.files: raise KeyError - if not rfile: rfile = rawsfile(header=filename) - self.files[filename] = rfile - rfile.dir = self - return rfile + raise ValueError('Failed to add file because no recognized arguments were specificed.') + + def addbyauto(self, auto, **kwargs): + if isinstance(auto, rawsbasefile): + return self.addbyfile(auto, **kwargs) + elif isinstance(auto, basestring): + if os.path.isfile(auto): + return self.addbyfilepath(auto, **kwargs) + elif os.path.isdir(auto): + return self.addbydirpath(auto, **kwargs) + else: + return self.addbyname(auto, **kwargs) + elif isinstance(auto, rawsdir): + return self.addbydir(auto, **kwargs) - def setfile(self, filename=None, rfile=None): - if rfile and not filename: filename = rfile.header - rfile.dir = self - self.files[filename] = rfile - - def removefile(self, filename=None, rfile=None): - if not rfile.dir == self: raise ValueError - if rfile and not filename: filename = rfile.header - rfile.dir = None - del self.files[filename] - - def addpath(self, path): - with open(path, 'rb') as rfilestream: - rfile = rawsfile(path=path, rfile=rfilestream, dir=self) - if rfile.header in self.files: raise ValueError - self.files[rfile.header] = rfile - return rfile - - def __getitem__(self, name): return self.getfile(name) - def __setitem__(self, name, value): return self.setfile(name, value) - def __contains__(self, name): return name in self.files + def addbyfile(self, file, **kwargs): + self.addfiletodicts(file, **kwargs) + return file + def addbyname(self, name, ext=None, loc=None, kind=None, **kwargs): + file = self.filebyname(name=name, ext=ext, loc=loc, kind=kind) + self.addfiletodicts(file, **kwargs) + return file + def addbyfilepath(self, path, root=None, loc=None, kind=None, **kwargs): + file = self.filebyfilepath(path=path, root=root, loc=loc, kind=kind) + self.addfiletodicts(file, **kwargs) + return file + def addbydirpath(self, path, root=None, loc=None, kind=None, **kwargs): + files = self.filesbydirpath(path=path, root=root, loc=loc, kind=kind) + self.addfilestodicts(files, **kwargs) + return files + def addbydir(self, dir, loc=None, **kwargs): + files = self.filesbydir(dir=dir, loc=loc) + self.addfilestodicts(files, **kwargs) + return files + def addbytokens(self, name, tokens, **kwargs): + file = self.addbyname(name, **kwargs) + file.add(tokens) + return file + def addbybincontent(self, name, content, **kwargs): + file = self.addbyname(name, kind=rawsbinfile, **kwargs) + file.content = content + return file + + def filebyname(self, name, ext=None, loc=None, kind=None): + if kind is None: kind = rawsfile + splitloc, name = os.path.split(name) + if not ext: name, ext = os.path.splitext(name) + loc = os.path.join(loc, splitloc) if loc else splitloc + return kind(name=name, ext=ext, loc=loc, dir=self) + def filebyfilepath(self, path, root=None, loc=None, kind=None): + if kind is None: kind = rawsfile + return kind(path=path, loc=loc, dir=self) + def filesbydirpath(self, path, root=None, loc=None, kind=None): + for walkroot, walkdirs, walkfiles in os.walk(path): + return ((kind if kind else rawsbasefile.factory)(path=os.path.join(walkroot, walkfile), root=root, loc=loc, dir=self) for walkfile in walkfiles) + def filesbydir(self, dir, loc=None): + for dirfile in dir.files.iteritems(): + newfile = dirfile.copy() + newfile.dir = self + newfile.reloc(loc) + yield newfile + + def addtodicts(self, file, replace=False): + if isinstance(file, rawsbasefile): + self.addfiletodicts(file) + else: + self.addfilestodicts(file) + def addfilestodicts(self, files, replace=False): + for file in files: self.addfiletodicts(file, replace) + def addfiletodicts(self, file, replace=False): + '''Internal: Used to add a file to files and filenames dictionaries.''' + + if file.dir is not self and file.dir is not None: + raise ValueError('Failed to add file %s to dir because it already belongs to another dir. You probably meant to remove the file first or to add a copy.' % file) + + if str(file) in self.files: + if not replace: raise KeyError('Failed to add file %s to dir because it already contains a file by the same name.' % file) + self.remove(self.files[str(file)]) + + file.dir = self + + self.files[str(file)] = file + + if file.name not in self.filenames: self.filenames[file.name] = [] + self.filenames[file.name].append(file) + + def remove(self, file=None): + if file is None: raise KeyError('Failed to remove file because no file was given.') + if isinstance(file, basestring): file = self.getfile(file) + + if file.dir is not self: raise KeyError('Failed to remove file because it belongs to a different dir.') + if not any(file is f for f in self.iterfiles()): raise KeyError('Failed to remove file because it doesn\'t belong to this dir.') + + self.filenames[file.name].remove(file) + self.files[str(file)].dir = None + del self.files[str(file)] + + def addfile(self, filename=None, rfile=None, path=None): + '''Deprecated: As of v1.0.2. Use the add method instead.''' + return self.add(file=filename if filename is not None else rfile, path=path) + def removefile(self, name=None, file=None): + '''Deprecated: As of v1.0.2. Use the remove method instead.''' + return self.remove(file if file is not None else name) - def read(self, path, log=None): + def read(self, root=None, paths=None): '''Reads raws from all text files in the specified directory.''' - for filename in os.listdir(path): - filepath = os.path.join(path, filename) - if filename.endswith('.txt') and os.path.isfile(filepath): - if log: log.debug('Reading raws file %s.' % filepath) - with open(filepath, 'rb') as rfile: - filenamekey = os.path.splitext(os.path.basename(filename))[0] - self.files[filenamekey] = rawsfile(path=filepath, rfile=rfile, dir=self) + + if root is None: + if self.root is None: raise ValueError('Failed to read dir because no root directory was specified.') + root = self.root + + if paths is None: paths = self.paths + if paths is not None: + paths = (paths,) if isinstance(paths, basestring) else paths + else: + paths = os.listdir(root) + + addeddirs = {} + + for path in paths: + path = os.path.join(root, path) + + if os.path.isdir(path): + for walkroot, walkdirs, walkfiles in os.walk(path): + + # Add files + for name in walkfiles: + filepath = os.path.join(walkroot, name) + file = rawsbasefile.factory(filepath, root=root, dir=self) + addeddirs[os.path.abspath(os.path.dirname(filepath)).replace('\\', '/')] = True + self.add(file) + + # Add empty directories + for dir in walkdirs: + dir = os.path.abspath(os.path.join(walkroot, dir)).replace('\\', '/') + if not any([added.startswith(dir) for added in addeddirs.iterkeys()]): + file = rawsbasefile.factory(path=dir, root=root, dir=self) + self.add(file) + + elif os.path.isfile(path): + file = rawsbasefile.factory(path, root=root, dir=self) + addeddirs[os.path.abspath(os.path.dirname(path))] = True + self.add(file) + else: - if log: log.debug('Found non-raws file %s.' % filepath) - self.otherfiles.append((path, filename)) + raise ValueError('Failed to read dir because a bad path %s was provided.' % path) - def write(self, path, log=None): + def write(self, dest=None): '''Writes raws to the specified directory.''' - - # Write raws files - for filename in self.files: - filepath = os.path.join(path, filename) - if not filepath.endswith('.txt'): filepath += '.txt' - with open(filepath, 'wb') as rfile: - if log: log.debug('Writing raws file %s.' % filepath) - self.files[filename].write(rfile) - # Copy other files - for filepath, filename in self.otherfiles: - if filepath != path: - originalpath = os.path.join(filepath, filename) - writepath = os.path.join(path, filename) - if log: log.debug('Writing non-raws file %s.' % writepath) - if os.path.isfile(originalpath): - shutil.copy2(originalpath, writepath) - elif os.path.isdir(originalpath): - copytree(originalpath, writepath) - elif log: - log.error('Failed to write non-raws file %s: it\'s neither a file nor a directory.' % writepath) + dest = self.getdestforfileop(dest) + if self.log: self.log.debug('Writing %d files to %s.' % (len(self.files), dest)) + for file in self.files.itervalues(): + file.write(dest) + + def clean(self, dest=None): + dest = self.getdestforfileop(dest) + if self.log: self.log.debug('Cleaning files in %s.' % dest) + for path in self.paths: + path = os.path.join(dest, path) + if os.path.isfile(path): + os.remove(path) + elif os.path.isdir(path): + shutil.rmtree(path) + + def getdestforfileop(self, dest, exception=True): + '''Internal''' + if dest is None: + dest = self.dest if self.dest else self.root + if exception and dest is None: raise ValueError('Failed to write dir because no destination path was specified.') + return dest - def tokens(self): + def tokens(self, *args, **kwargs): '''Iterate through all tokens.''' - for filename in self.files: - for token in self.files[filename].tokens(): - yield token + for file in self.files.itervalues(): + if isinstance(file, rawsqueryable): + for token in file.tokens(*args, **kwargs): yield token def getobjheaders(self, type): '''Gets OBJECT:X tokens where X is type. Is also prepared for special cases @@ -127,23 +309,9 @@ def getobjheaders(self, type): match_types = self.getobjheadername(type) results = rawstokenlist() - for rfile in self.files.itervalues(): - root = rfile.root() - if root is not None and root.value == 'OBJECT' and root.nargs() == 1 and root.args[0] in match_types: - results.append(root) + for file in self.files.itervalues(): + if isinstance(file, rawsqueryable): + root = file.root() + if root is not None and root.value == 'OBJECT' and root.nargs() == 1 and root.args[0] in match_types: + results.append(root) return results - - - -# credit belongs to http://stackoverflow.com/a/13814557/3478907 -def copytree(src, dst, symlinks=False, ignore=None): - if not os.path.exists(dst): - os.makedirs(dst) - for item in os.listdir(src): - s = os.path.join(src, item) - d = os.path.join(dst, item) - if os.path.isdir(s): - copytree(s, d, symlinks, ignore) - else: - if not os.path.exists(d) or os.stat(src).st_mtime - os.stat(dst).st_mtime > 1: - shutil.copy2(s, d) diff --git a/raws/file.py b/raws/file.py index d0b6f4c..c3d0365 100644 --- a/raws/file.py +++ b/raws/file.py @@ -1,35 +1,273 @@ -from queryable import rawsqueryable_obj, rawstokenlist +import os +import re +import shutil + +from copytree import copytree +from queryable import rawsqueryableobj, rawstokenlist from token import rawstoken -class rawsfile(rawsqueryable_obj): + + +class rawsbasefile(object): + def __init__(self): + self.dir = None + self.path = None + self.rootpath = None + self.loc = None + self.name = None + self.ext = None + self.kind = None + + @staticmethod + def factory(path, **kwargs): + if path.endswith('.txt'): + with open(path, 'rb') as txt: + if txt.readline().strip() == os.path.splitext(os.path.basename(path))[0]: + txt.seek(0) + return rawsfile(path=path, file=txt, **kwargs) + if os.path.basename(path) in ('dfhack.init', 'dfhack.init-example'): + return rawsbinfile(path=path, **kwargs) + return rawsreffile(path=path, **kwargs) + + def __str__(self): + name = ''.join((self.name, self.ext)) if self.ext and self.name else self.name + path = os.path.join(self.loc, name) if self.loc and name else name + return path.replace('\\', '/') if path else '' + def __repr__(self): + return str(self) + + def __hash__(self): + return hash(str(self)) + + def __eq__(self, other): + return str(self) == str(other) + def __ne__(self, other): + return str(self) != str(other) + + def __gt__(self, other): + return str(self) > str(other) + def __ge__(self, other): + return str(self) >= str(other) + def __lt__(self, other): + return str(self) < str(other) + def __le__(self, other): + return str(self) <= str(other) + + def setpath(self, path, root=None, loc=None, name=None, ext=None): + if self.dir and self.dir.root and (not root): root = self.dir.root + path = os.path.abspath(path) if path else None + root = os.path.abspath(root) if root else None + self.path = path + self.rootpath = root + if not path: + self.name, self.ext = None, None + elif os.path.isfile(path): + self.name, self.ext = os.path.splitext(os.path.basename(path)) + else: + self.name, self.ext = os.path.basename(path), None + if root and path and root != path and path.startswith(root): + self.loc = os.path.dirname(os.path.relpath(path, root)) + else: + self.loc = None + if loc: self.loc = loc + if name: self.name = name + if ext: self.ext = ext + self.kind = self.ext[1:] if self.ext else 'dir' + + def reloc(self, loc): + if loc and self.loc: + self.loc = os.path.join(loc, self.loc) + elif loc: + self.loc = loc + + def dest(self, path, makedir=False): + '''Internal: Given a root directory that this file would be written to, get the full path of where this file belongs.''' + dest = os.path.join(path, str(self)) + dir = os.path.dirname(dest) + if makedir and not os.path.isdir(dir): os.makedirs(dir) + return dest + + def remove(self): + '''Remove this file from the raws.dir object to which it belongs. + + Example usage: + >>> dwarf = df.getobj('CREATURE:DWARF') + >>> print dwarf + [CREATURE:DWARF] + >>> print dwarf.file + creature_standard + >>> dwarf.file.remove() + >>> print df.getobj('CREATURE:DWARF') + None + >>> print df.getfile('creature_standard') + None + ''' + if self.dir is not None: + self.dir.remove(self) + else: + raise ValueError('Failed to remove file because it doesn\'t belong to any dir.') + + + +class rawsreffile(rawsbasefile): + def __init__(self, path=None, dir=None, root=None, **kwargs): + self.dir = dir + self.setpath(path, root, **kwargs) + + def copy(self): + copy = rawsotherfile() + copy.path = self.path + copy.dir = self.dir + copy.rootpath = self.rootpath + copy.name = self.name + copy.ext = self.ext + copy.loc = self.loc + return copy + + def ref(self): + return self + def bin(self): + self.kind = 'bin' + self.__class__ = rawsbinfile + self.read() + return self + def raw(self): + self.kind = 'raw' + self.__class__ = rawsfile + self.read() + return self + + def write(self, path): + dest = self.dest(path, makedir=True) + if self.path != dest: + if os.path.isfile(self.path): + shutil.copy2(self.path, dest) + elif os.path.isdir(self.path): + copytree(self.path, dest) + else: + raise ValueError('Failed to write file because its path %s refers to neither a file nor a directory.' % self.path) + + + +class rawsbinfile(rawsreffile): + def __init__(self, content=None, path=None, dir=None, **kwargs): + self.dir = None + self.setpath(path, **kwargs) + self.dir = dir + self.content = content + if self.content is None and self.path is not None and os.path.isfile(self.path): self.read(self.path) + + def read(self, path=None): + with open(path if path else self.path, 'rb') as binfile: self.content = binfile.read() + + def ref(self): + raise ValueError('Failed to cast binary file to reference file because it is an invalid conversion.') + def bin(self): + return self + def raw(self): + self.kind = 'raw' + self.__class__ = rawsfile + self.settokens(rawstoken.parse(self.content)) + return self + + def copy(self): + copy = rawsbinfile() + copy.path = self.path + copy.dir = self.dir + copy.rootpath = self.rootpath + copy.name = self.name + copy.ext = self.ext + copy.loc = self.loc + copy.content = self.content + return copy + + def __repr__(self): + return str(self.content) + + def __len__(self): + return len(self.content) + + def write(self, path): + dest = self.dest(path, makedir=True) + with open(dest, 'wb') as file: + file.write(self.content) + + def add(self, content): + self.content += content + + + +class rawsfile(rawsbasefile, rawsqueryableobj): '''Represents a single file within a raws directory.''' - def __init__(self, header=None, data=None, path=None, tokens=None, rfile=None, dir=None): + def __init__(self, name=None, file=None, path=None, root=None, content=None, tokens=None, dir=None, readpath=True, **kwargs): '''Constructs a new raws file object. - header: The header string to appear at the top of the file. Also used to determine filename. + name: The name string to appear at the top of the file. Also used to determine filename. data: A string to be parsed into token data. path: A path to the file from which this object is being parsed, if any exists. tokens: An iterable of tokens from which to construct the object; these tokens will be its initial contents. - rfile: A file-like object from which to automatically read the header and data attributes. + file: A file-like object from which to automatically read the name and data attributes. dir: Which raws.dir object this file belongs to. ''' - if rfile: - self.read(rfile) - if header is not None: self.header = header - if data is not None: self.data = data - else: - self.header = header - self.data = data - self.path = path + + self.dir = dir + self.data = None + self.setpath(path=path, root=root, **kwargs) + self.roottoken = None self.tailtoken = None - self.dir = dir + + if file: + self.read(file) + elif path and readpath: + self.read(path) + + if name is not None: self.name = name + if content is not None: self.data = content + if self.data is not None: tokens = rawstoken.parse(self.data, implicit_braces=False, file=self) self.settokens(tokens, setfile=False) elif tokens is not None: self.settokens(tokens, setfile=True) + + if name: self.name = name + + if (not self.path) and (not self.ext): self.ext = '.txt' + self.kind = 'raw' + + def __enter__(self): + return self + def __exit__(self): + if self.path: self.write(self.path) + + def __eq__(self, other): + return self.equals(other) + def __ne__(self, other): + return not self.equals(other) + + def __len__(self): + return self.length() + + def __nonzero__(self): + return True + + def __repr__(self): + return self.content() + + def content(self): + return '%s\n%s' %(self.name, ''.join([repr(o) for o in self.tokens()])) + + def ref(self): + raise ValueError('Failed to cast binary file to reference file because it is an invalid conversion.') + def bin(self): + self.kind = 'bin' + self.content = self.content() + self.__class__ = rawsbinfile + return self + def raw(self): + return self def index(self, index): itrtoken = self.root() if index >= 0 else self.tail() @@ -41,35 +279,33 @@ def index(self, index): def getpath(self): return self.path - def setpath(self, path): - self.path = path - def getheader(self): - '''Get the file header. + def getname(self): + '''Get the file name. Example usage: >>> dwarf = df.getobj('CREATURE:DWARF') >>> creature_standard = dwarf.file - >>> print creature_standard.getheader() + >>> print creature_standard.getname() creature_standard >>> creature_standard.setheader('example_header') - >>> print creature_standard.getheader() + >>> print creature_standard.getname() example_header ''' - return self.header - def setheader(self, header): - '''Set the file header. + return self.name + def setname(self, name): + '''Set the file name. Example usage: >>> dwarf = df.getobj('CREATURE:DWARF') >>> creature_standard = dwarf.file - >>> print creature_standard.getheader() + >>> print creature_standard.getname() creature_standard >>> creature_standard.setheader('example_header') - >>> print creature_standard.getheader() + >>> print creature_standard.getname() example_header ''' - self.header = header + self.name = name def settokens(self, tokens, setfile=True): '''Internal: Utility method for setting the root and tail tokens given an iterable.''' @@ -101,26 +337,18 @@ def copy(self): >>> print item_food == food_copy False ''' - rfile = rawsfile(header=self.header, path=self.path, dir=self.dir) - rfile.settokens(rawstoken.copy(self.tokens())) - return rfile + copy = rawsfile() + copy.path = self.path + copy.rootpath = self.rootpath + copy.name = self.name + copy.ext = self.ext + copy.loc = self.loc + copy.settokens(rawstoken.copy(self.tokens())) + return copy def equals(self, other): return rawstoken.tokensequal(self.tokens(), other.tokens()) - def __eq__(self, other): - return self.equals(other) - def __ne__(self, other): - return not self.equals(other) - - def __len__(self): - return self.length() - - def __str__(self): - return self.header - def __repr__(self): - return '%s\n%s' %(self.header, ''.join([repr(o) for o in self.tokens()])) - def root(self): '''Gets the first token in the file. @@ -160,12 +388,24 @@ def tokens(self, reverse=False, **kwargs): for token in generator: yield token - def read(self, rfile): - '''Internal: Given a file-like object, reads header and data from it.''' - self.header, self.data = rfile.readline().strip(), rfile.read() - def write(self, rfile): - '''Internal: Given a file-like object, writes the file's contents to that file.''' - rfile.write(self.__repr__()) + def read(self, file=None): + '''Given a path or file-like object, reads name and data.''' + if file is None: file = self.path + if isinstance(file, basestring): + self.path = file + self.ext = os.path.splitext(file)[1] + with open(file, 'rb') as src: + self.name, self.data = src.readline().strip(), src.read() + else: + self.name, self.data = file.readline().strip(), file.read() + + def write(self, file): + '''Given a path to a directory or a file-like object, writes the file's contents to that file.''' + if isinstance(file, basestring): + with open(self.dest(file, makedir=True), 'wb') as dest: + dest.write(self.content()) + else: + file.write(self.content()) def add(self, auto=None, pretty=None, token=None, tokens=None, **kwargs): '''Adds tokens to the end of a file. @@ -183,7 +423,7 @@ def add(self, auto=None, pretty=None, token=None, tokens=None, **kwargs): [ITEM_FOOD:ITEM_FOOD_ROAST] [NAME:roast] [LEVEL:4] - >>> tokens = item_food.add('\nhi! [THIS][IS][AN][EXAMPLE]') + >>> tokens = item_food.add('hi! [THIS][IS][AN][EXAMPLE]') >>> print tokens hi! [THIS][IS][AN][EXAMPLE] >>> print item_food.list() @@ -196,8 +436,7 @@ def add(self, auto=None, pretty=None, token=None, tokens=None, **kwargs): [LEVEL:3] [ITEM_FOOD:ITEM_FOOD_ROAST] [NAME:roast] - [LEVEL:4] - hi! [THIS][IS][AN][EXAMPLE] + [LEVEL:4]hi! [THIS][IS][AN][EXAMPLE] ''' tail = self.tail() if tail: @@ -215,23 +454,6 @@ def add(self, auto=None, pretty=None, token=None, tokens=None, **kwargs): elif tokens is not None: self.settokens(tokens) return tokens - - def remove(self): - '''Remove this file from the raws.dir object to which it belongs. - - Example usage: - >>> dwarf = df.getobj('CREATURE:DWARF') - >>> print dwarf - [CREATURE:DWARF] - >>> print dwarf.file - creature_standard - >>> dwarf.file.remove() - >>> print df.getobj('CREATURE:DWARF') - None - >>> print df.getfile('creature_standard') - None - ''' - self.dir.removefile(rfile=self) def length(self): '''Get the number of tokens in this file. @@ -265,7 +487,6 @@ def clear(self): self.tailtoken = None def getobjheaders(self, type): + match_types = self.getobjheadername(type) root = self.root() - if root is not None and root.value == 'OBJECT' and root.nargs() == 1 and root.args[0] in match_types: - return (root,) - return tuple() + return (root,) if root is not None and root.value == 'OBJECT' and root.nargs(1) and root.args[0] in match_types else tuple() diff --git a/raws/filters.py b/raws/filters.py index 5545eaf..034c2b6 100644 --- a/raws/filters.py +++ b/raws/filters.py @@ -3,7 +3,7 @@ -# Hackish solution to cyclic import +# Hackish solution to cyclic import (the struggle is real) def rawstoken(): if 'token' not in globals(): import token return token.rawstoken @@ -118,13 +118,16 @@ def __init__(self, True. ''' self.pretty = pretty + if pretty: token = rawstoken().parseone(pretty) exact_value = token.value if token.nargs(): exact_args = token.args + if match_token: exact_value = match_token.value exact_args = match_token.args + self.exact_token = exact_token self.exact_value = exact_value self.except_value = except_value @@ -148,7 +151,14 @@ def __init__(self, self.limit = limit self.limit_terminates = limit_terminates - # Anchor regular expressions + # Make exact_arg, re_arg, and arg_in easier: Allow a single iterable with index and value, don't always require an iterable of them. + if self.exact_arg and isinstance(self.exact_arg[0], int): self.exact_arg = (self.exact_arg,) + if self.re_arg and isinstance(self.re_arg[0], int): self.re_arg = (self.re_arg,) + if self.arg_in and isinstance(self.arg_in[0], int): self.arg_in = (self.arg_in,) + + self.anchor() + + def anchor(self): if self.re_value: self.re_value += '$' if self.re_prefix: self.re_prefix += '$' if self.re_suffix: self.re_suffix += '$' diff --git a/raws/objects.py b/raws/objects.py new file mode 100644 index 0000000..6989f43 --- /dev/null +++ b/raws/objects.py @@ -0,0 +1,128 @@ +def getoption(version, *options): + '''Internal: Used by objects functions to get an option corresponding to some Dwarf Fortress version.''' + if version: + if isinstance(version, basestring): # For strings + pass + elif 'config' in version.__dict__ and 'version' in version.config.__dict__: # For dirs + version = rawsdir.config.version + elif 'version' in version.__dict__: # For configs + version = version.version + else: + version = str(version) + if version and version.startswith('0.2'): + return options[1] + return options[0] + +def objectsdict(headersdict): + '''Internal: Used to build a dict mapping objects to their headers from a dict mapping headers to their possible objects.''' + objdict = {} + for header, objects in headersdict.iteritems(): + for obj in objects: objdict[obj] = header + return objdict + + + +headers_base = { + 'BODY': [ + 'BODY', + ], + 'CREATURE': [ + 'CREATURE', + ], + 'ENTITY': [ + 'ENTITY', + ], + 'GRAPHICS': [ + 'TILE_PAGE', 'CREATURE_GRAPHICS', + ], + 'ITEM': [ + 'ITEM_WEAPON', 'ITEM_TOY', 'ITEM_TOOL', 'ITEM_INSTRUMENT', + 'ITEM_TRAPCOMP', 'ITEM_ARMOR', 'ITEM_AMMO', 'ITEM_SIEGEAMMO', + 'ITEM_GLOVES', 'ITEM_SHOES', 'ITEM_SHIELD', 'ITEM_HELM', + 'ITEM_PANTS', 'ITEM_FOOD', + ], + 'LANGUAGE': [ + 'WORD', 'SYMBOL', 'TRANSLATION', + ], + 'REACTION': [ + 'REACTION', + ], + 'TISSUE_TEMPLATE': [ + 'TISSUE_TEMPLATE', + ], +} + +headers_23 = { + 'DESCRIPTOR': [ + 'COLOR', 'SHAPE' + ], + 'MATGLOSS': [ + 'MATGLOSS_GEM', 'MATGLOSS_PLANT', 'MATGLOSS_STONE', 'MATGLOSS_WOOD' + ], +} +headers_23.update(headers_base) + +headers_31 = { + 'BODY_DETAIL_PLAN': [ + 'BODY_DETAIL_PLAN' + ], + 'BUILDING': [ + 'BUILDING_WORKSHOP', 'BUILDING_FURNACE' + ], + 'CREATURE_VARIATION': [ + 'CREATURE_VARIATION' + ], + 'DESCRIPTOR_COLOR': [ + 'COLOR' + ], + 'DESCRIPTOR_PATTERN': [ + 'COLOR_PATTERN' + ], + 'DESCRIPTOR_SHAPE': [ + 'SHAPE' + ], + 'INORGANIC': [ + 'INORGANIC' + ], + 'INTERACTION': [ + 'INTERACTION' + ], + 'MATERIAL_TEMPLATE': [ + 'MATERIAL_TEMPLATE' + ], + 'PLANT': [ + 'PLANT' + ], +} +headers_31.update(headers_base) + +objects_23 = objectsdict(headers_23) +objects_31 = objectsdict(headers_31) + + + +def headers(version=None): + '''Get a list of valid object types as given by the OBJECT:TYPE tokens which appear + at the beginning of raws files.''' + return getoption(version, headers_31, headers_23).keys() +def objects(version=None): + '''Get a list of valid object types as given by the TYPE:ID tokens which denote the + beginning of an object definition.''' + return getoption(version, objects_31, objects_23).keys() + +def headerdict(version=None): + '''Get the header dict corresponding to some version. The dict maps headers as keys + to lists of corresponding object types. For example, BUILDING to BUILDING_WORKSHOP + and BUILDING_FURNACE.''' + return getoption(version, headers_31, headers_23) +def objectdict(version=None): + '''Get the object dict corresponding to some version. The dict maps object types as + keys to their corresponding header. For example, BUILDING_WORKSHOP to BUILDING.''' + return getoption(version, objects_31, objects_23) + +def headerforobject(type, version=None): + '''Returns the header for a particular object type given a version.''' + return objectdict(version)[type] +def objectsforheader(header, version=None): + '''Returns the object types corresponding to a particular header given a version.''' + return headerdict(version)[header] diff --git a/raws/queryable.py b/raws/queryable.py index 8cc2f9f..942684c 100644 --- a/raws/queryable.py +++ b/raws/queryable.py @@ -1,7 +1,9 @@ # vim:fileencoding=UTF-8 import inspect -from filters import * + +import objects +from filters import rawstokenfilter @@ -13,11 +15,11 @@ class rawsqueryable(object): its limit or the tokens have run out.''' quick_query_args_docstring = ''' - %s pretty: Convenience argument which acts as a substitute for directly assigning a filter's exact_value and exact_args arguments. Some methods also accept an until_pretty argument which acts as a substitute for until_exact_value and until_exact_args. + %s **kwargs: If no tokeniter is specified, then arguments which correspond to named arguments of the object's tokens method will be passed to that method. All other arguments will be passed to the appropriate filters, @@ -29,7 +31,8 @@ class rawsqueryable(object): named normally.) ''' % query_tokeniter_docstring - def __iter__(self): return self.tokens() + def __iter__(self): + return self.tokens() def __contains__(self, item): if isinstance(item, basestring): @@ -38,15 +41,41 @@ def __contains__(self, item): return item in self.tokens() def __getitem__(self, item): - if isinstance(item, basestring): + '''Overrides object[...] behavior. Accepts a number of different types for the item argument, each resulting in different behavior. + + object[...] + Returns the same as object.list(). + object[str] + Returns the same as object.get(str). + object[int] + Returns the same as object.index(int). + object[slice] + Returns the same as object.slice(slice). + object[iterable] + Returns a flattened list containing object[member] in order for each member of iterable. + object[anything else] + Raises an exception. + ''' + if item is Ellipsis: + return self.list() + elif isinstance(item, basestring): return self.get(pretty=item) elif isinstance(item, int): return self.index(item) elif isinstance(item, slice): return self.slice(item) + elif hasattr(item, '__iter__') or hasattr(item, '__getitem__'): + return self.getitems(items) else: - raise ValueError - + raise ValueError('Failed to get item because the argument was of an unrecognized type.') + + def getitems(self, items): + result = [] + for item in items: + ext = self.__getitem__(item) + (result.extend if isinstance(ext, list) else result.append)(ext) + return result + def slice(self, slice): return rawstokenlist(self.islice(slice)) @@ -58,14 +87,65 @@ def islice(self, slice): yield token else: return + + def removefirst(self, *args, **kwargs): + token = self.get(*args, **kwargs) + if token is not None: token.remove() + return token + def removelast(self, *args, **kwargs): + token = self.getlast(*args, **kwargs) + if token is not None: token.remove() + return token + def removeuntil(self, *args, **kwargs): + token = self.until(*args, **kwargs) + if token is not None: token.remove() + return token + def removefirstuntil(self, *args, **kwargs): + token = self.getuntil(*args, **kwargs) + if token is not None: token.remove() + return token + def removelastuntil(self, *args, **kwargs): + token = self.getlastuntil(*args, **kwargs) + if token is not None: token.remove() + return token + + def removeall(self, *args, **kwargs): + tokens = self.all(*args, **kwargs) + for token in tokens: token.remove() + return tokens + def removeuntil(self, *args, **kwargs): + tokens = self.until(*args, **kwargs) + for token in tokens: token.remove() + return tokens + def removealluntil(self, *args, **kwargs): + tokens = self.alluntil(*args, **kwargs) + for token in tokens: token.remove() + return tokens + + def removeprop(self, *args, **kwargs): + token = self.getprop(*args, **kwargs) + if token is not None: token.remove() + return token + def removelastprop(self, *args, **kwargs): + token = self.getlastprop(*args, **kwargs) + if token is not None: token.remove() + return token + def removeallprop(self, *args, **kwargs): + tokens = self.allprop(*args, **kwargs) + for token in tokens: token.remove() + return tokens + def removeselfandprops(self, *args, **kwargs): + tokens = [self] + self.removeallprop(*args, **kwargs) + self.remove() + return tokens def query(self, filters, tokeniter=None, **kwargs): '''Executes a query on some iterable containing tokens. filters: A dict or other iterable containing rawstokenfilter-like objects. + %s **kwargs: If tokeniter is not given, then the object's token method will be called with these arguments and used instead. - %s ''' % rawsqueryable.query_tokeniter_docstring if tokeniter is None: tokeniter = self.tokens(**kwargs) @@ -249,7 +329,7 @@ def alluntil(self, pretty=None, until=None, tokeniter=None, **kwargs): ) return self.query(filters, tokeniter, **tokens_args)[1].result - def getprop(self, pretty=None, **kwargs): + def getprop(self, *args, **kwargs): '''Gets the first token matching the arguments, but stops at the next token with the same value as this one. Should be sufficient in almost all cases to get a token representing a property of an object, when @@ -265,9 +345,9 @@ def getprop(self, pretty=None, **kwargs): ''' until_exact_value, until_re_value, until_value_in = self.argsprops() - return self.getuntil(pretty=pretty, until_exact_value=until_exact_value, until_re_value=until_re_value, until_value_in=until_value_in, **kwargs) + return self.getuntil(*args, until_exact_value=until_exact_value, until_re_value=until_re_value, until_value_in=until_value_in, **kwargs) - def getlastprop(self, pretty=None, **kwargs): + def getlastprop(self, *args, **kwargs): '''Gets the last token matching the arguments, but stops at the next token with the same value as this one. Should be sufficient in almost all cases to get a token representing a property of an object, when @@ -283,9 +363,9 @@ def getlastprop(self, pretty=None, **kwargs): ''' until_exact_value, until_re_value, until_value_in = self.argsprops() - return self.getlastuntil(pretty=pretty, until_exact_value=until_exact_value, until_re_value=until_re_value, until_value_in=until_value_in, **kwargs) + return self.getlastuntil(*args, until_exact_value=until_exact_value, until_re_value=until_re_value, until_value_in=until_value_in, **kwargs) - def allprop(self, pretty=None, **kwargs): + def allprop(self, *args, **kwargs): '''Gets the all tokens matching the arguments, but stops at the next token with the same value as this one. Should be sufficient in almost all cases to get a token representing a property of an object, when @@ -302,7 +382,7 @@ def allprop(self, pretty=None, **kwargs): ''' until_exact_value, until_re_value, until_value_in = self.argsprops() - return self.alluntil(pretty=pretty, until_exact_value=until_exact_value, until_re_value=until_re_value, until_value_in=until_value_in, **kwargs) + return self.alluntil(*args, until_exact_value=until_exact_value, until_re_value=until_re_value, until_value_in=until_value_in, **kwargs) def propdict(self, always_list=True, value_keys=True, full_keys=True, **kwargs): '''Returns a dictionary with token values mapped as keys to the tokens @@ -386,41 +466,46 @@ def argstokens(self, tokeniter, kwargs): def argsprops(self): # Utility function for handling arguments of getprop, allprop, and propdict methods + # TODO: refactor a bit so that the obviated until_exact_value and until_re_value are no longer returned until_exact_value = None until_re_value = None - until_value_in = None - if self.value.startswith('ITEM_'): - until_re_value = 'ITEM_.+' - elif self.value == 'WORD' or self.value == 'SYMBOL': - until_value_in = ('WORD', 'SYMBOL') - else: - until_exact_value = self.value + until_value_in = objects.objectsforheader(objects.headerforobject(self.value)) return until_exact_value, until_re_value, until_value_in -class rawsqueryable_obj(rawsqueryable): +class rawsqueryableobj(rawsqueryable): def __init__(self): self.files = None def getobjheadername(self, type): - # Utility function fit for handling objects as of 0.40.24 - if type in ('WORD', 'SYMBOL', 'TRANSLATION'): - return ('LANGUAGE',) - elif type.startswith('ITEM_'): - return ('ITEM',) - elif type == 'COLOR' or type == 'SHAPE': - return ('DESCRIPTOR', 'DESCRIPTOR_%s' % type) - elif type == 'COLOR_PATTERN': - return ('DESCRIPTOR_PATTERN',) - elif type.startswith('MATGLOSS_'): - return ('MATGLOSS',) - elif type in ('TILE_PAGE', 'CREATURE_GRAPHICS'): - type = ('GRAPHICS',) + version = self if hasattr(self, 'config') or hasattr(self, 'version') else self.dir + if type is None: + return objects.headers(version) else: - return type + return (objects.headerforobject(type, version),) - def getobj(self, pretty=None, type=None, exact_id=None): + def headersfortype(self, type=None, type_in=None): + if type or (type_in is None): + headers = self.getobjheaders(type) + else: + headers = [] + if type_in: + for itertype in type_in: + for header in self.getobjheaders(itertype): + if not any(header is h for h in headers): headers.append(header) + return headers + + def removeobj(self, *args, **kwargs): + obj = self.getobj(*args, **kwargs) + if obj: obj.removeselfandprops() + return obj + def removeallobj(self, *args, **kwargs): + objects = self.allobj(*args, **kwargs) + for obj in objects: obj.removeselfandprops() + return objects + + def getobj(self, pretty=None, type=None, exact_id=None, type_in=None, re_id=None, id_in=None): '''Get the first object token matching a given type and id. (If there's more than one result for any given query then I'm afraid you've done something silly with your raws.) This method should work properly with things like @@ -442,13 +527,22 @@ def getobj(self, pretty=None, type=None, exact_id=None): [WEAPON:ITEM_WEAPON_AXE_BATTLE] ''' - type, exact_id = rawsqueryable_obj.objpretty(pretty, type, exact_id) - for objecttoken in self.getobjheaders(type): - obj = objecttoken.get(exact_value=type, exact_args=(exact_id,)) + type, exact_id = rawsqueryableobj.objpretty(pretty, type, exact_id) + headers = self.headersfortype(type, type_in) + if type is None and type_in is None: type_in = objects.objects() + for objecttoken in headers: + obj = objecttoken.get( + exact_value = type, + value_in = type_in, + exact_args = (exact_id,) if exact_id else None, + re_args = (re_id,) if re_id else None, + arg_in = ((0, id_in),) if id_in else None, + args_count = 1 + ) if obj: return obj return None - def allobj(self, pretty=None, type=None, exact_id=None, re_id=None, id_in=None): + def allobj(self, pretty=None, type=None, exact_id=None, type_in=None, re_id=None, id_in=None): '''Gets all objects matching a given type and optional id or id regex. Example usage: @@ -471,12 +565,14 @@ def allobj(self, pretty=None, type=None, exact_id=None, re_id=None, id_in=None): [CREATURE:BEAR_SLOTH] ''' - if re_id and id_in: raise ValueError - type, exact_id = rawsqueryable_obj.objpretty(pretty, type, exact_id) + type, exact_id = rawsqueryableobj.objpretty(pretty, type, exact_id) results = rawstokenlist() - for objecttoken in self.getobjheaders(type): + headers = self.headersfortype(type, type_in) + if type is None and type_in is None: type_in = objects.objects() + for objecttoken in headers: for result in objecttoken.all( exact_value = type, + value_in = type_in, exact_args = (exact_id,) if exact_id else None, re_args = (re_id,) if re_id else None, arg_in = ((0, id_in),) if id_in else None, @@ -508,12 +604,11 @@ def objdict(self, *args, **kwargs): @staticmethod def objpretty(pretty, type, id): - '''Internal''' - # Utility method for handling getobj/allobj arguments. + '''Internal: Used for handling getobj/allobj arguments.''' if pretty is not None: if ':' in pretty: parts = pretty.split(':') - if len(parts) != 2: raise ValueError + if len(parts) != 2: raise ValueError('Failed to parse argument because there were too many or too few colons, there ought to be be just one.') return parts[0], parts[1] elif type is None: return pretty, id @@ -521,18 +616,32 @@ def objpretty(pretty, type, id): return pretty, type else: return type, id - + class rawstokenlist(list, rawsqueryable): '''Extends builtin list with token querying functionality.''' - def tokens(self, range=None, include_self=False, reverse=False): - if include_self: raise ValueError + def tokens(self, range=None, reverse=False): for i in xrange(self.__len__()-1, -1, -1) if reverse else xrange(0, self.__len__()): if range is not None and range <= count: break yield self.__getitem__(i) + def add(self, item): + if isinstance(item, rawstoken): + self.append(item) + elif isinstance(item, rawsqueryable): + self.extend(item.tokens()) + elif isinstance(item, list): + self.extend(item) + else: + raise ValueError('Failed to add item because it was of an unrecognized type.') + + def each(self, fn, *args, **kwargs): + '''Calls a function for each entry in the list with that entry as the argument, and + appends each result to a returned tokenlist.''' + return rawstokenlist(fn(token, *args, **kwargs) for token in self) + def __str__(self): if len(self) == 0: return '' diff --git a/raws/token.py b/raws/token.py index 86a0a1b..3ef83bc 100644 --- a/raws/token.py +++ b/raws/token.py @@ -1,6 +1,7 @@ import itertools + +from tokenargs import tokenargs from queryable import rawsqueryable, rawstokenlist -from filters import rawstokenfilter class rawstoken(rawsqueryable): @@ -10,20 +11,12 @@ class rawstoken(rawsqueryable): detected automatically. If a rawstoken is specified it will be treated as a token argument. If a string, pretty. If anything else, tokens.''' - '''Don't allow these characters in values or arguments.''' - illegal_internal_chars = '[]:' + illegal_internal_chars = tokenargs.illegal # TODO: make this better '''Don't allow these characters in a token's prefix or suffix.''' illegal_external_chars = '[' - '''Automatically replace some illegal strings when passed to setarg with their legal equivalents.''' - argument_replacements = { - "':'": '58', - "'['": '91', - "']'": '93' - } - - def __init__(self, auto=None, pretty=None, token=None, value=None, args=None, prefix=None, suffix=None, prev=None, next=None, file=None): + def __init__(self, auto=None, pretty=None, token=None, value=None, args=None, arg=None, prefix=None, suffix=None, prev=None, next=None, file=None): '''Constructs a token object. %s (However, a tokens argument is illegal here and attempting to create @@ -53,25 +46,33 @@ def __init__(self, auto=None, pretty=None, token=None, value=None, args=None, pr False ''' % rawstoken.auto_arg_docstring + self.prev = prev # previous token sequentially + self.next = next # next token sequentially + + self.value = None + self.args = None + self.prefix = None + self.suffix = None + self.file = None + pretty, token, tokens = rawstoken.auto(auto, pretty, token, None) - if tokens is not None: raise ValueError + if tokens is not None: raise ValueError('Failed to initialize token object because the given argument was not a string or a single token.' % pretty) + if pretty is not None: token = rawstoken.parseone(pretty, implicit_braces=True) if token is not None: value = token.value - args = list(token.args) if token.args else [] + args = tokenargs(token.args) if token.args else tokenargs() prefix = token.prefix suffix = token.suffix - file = token.file - # tokens look like this: [value:arg1:arg2:...:argn] - self.prev = prev # previous token sequentially - self.next = next # next token sequentially - self.value = value # value for the token - self.args = args # arguments for the token - self.prefix = prefix # non-token text between the preceding token and this one - self.suffix = suffix # between this token and the next/eof (should typically apply to eof) - self.file = file # parent rawsfile object - if not self.args: self.args = [] + + if arg: self.setargs([arg]) + if value: self.setvalue(value) # value for the token + if args: self.setargs(args) # arguments for the token + if prefix: self.setprefix(prefix) # non-token text between the preceding token and this one + if suffix: self.setsuffix(suffix) # between this token and the next/eof (should typically apply to eof) + + if self.args is None: self.args = tokenargs() def __hash__(self): # Not that this class is immutable, just means you'll need to be careful about when you're using token hashes return hash('%s:%s' % (self.value, self.argsstr()) if self.nargs() else self.value) @@ -221,8 +222,7 @@ def __add__(self, other): tokens.extend(other) return tokens else: - raise ValueError - + raise ValueError('Failed to perform concatenation because the type of the other operand was unrecognized.') def __radd__(self, other): '''Internal: Same as __add__ except reversed.''' if isinstance(other, rawstoken): @@ -236,7 +236,7 @@ def __radd__(self, other): tokens.append(self) return tokens else: - raise ValueError + raise ValueError('Failed to perform concatenation because the type of the other operand was unrecognized.') def __mul__(self, value): '''Concatenates copies of this token the number of times specified. @@ -253,10 +253,16 @@ def __mul__(self, value): tokens.append(rawstoken.copy(self)) return tokens + def __iter__(self): + yield self.value + for arg in self.args: yield arg def __len__(self): return self.nargs() def __contains__(self, value): return self.containsarg(value) + + def __iadd__(self, value): + self.addarg(value) def __nonzero__(self): return True @@ -265,21 +271,15 @@ def __nonzero__(self): def auto(auto, pretty, token, tokens): '''Internal: Convenience function for handling method arguments''' if auto is not None: - if isinstance(auto, basestring): pretty = auto - elif isinstance(auto, rawstoken): token = auto - elif isinstance(auto, rawsqueryable): tokens = auto.tokens() - else: tokens = auto + if isinstance(auto, basestring): + pretty = auto + elif isinstance(auto, rawstoken): + token = auto + elif isinstance(auto, rawsqueryable): + tokens = auto.tokens() + else: + tokens = auto return pretty, token, tokens - - @staticmethod - def sanitizeargstring(value): - '''Internal: Utility method for sanitizing a string intended to be evaluated as an arugment for a token.''' - valuestr = str(value) - if valuestr in rawstoken.argument_replacements: - valuestr = rawstoken.argument_replacements[valuestr] - else: - if any([char in valuestr for char in rawstoken.illegal_internal_chars]): raise ValueError('Illegal character in argument: %s.' % valuestr) - return valuestr def index(self, index): itrtoken = self @@ -317,28 +317,6 @@ def nargs(self, count=None): ''' return len(self.args) if (count is None) else (len(self.args) == count) - def getarg(self, index=0): - '''Gets argument at index, returns None if the index is out of bounds. - - index: The argument index. - - Example usage: - >>> token = raws.token('EXAMPLE:argument 0:argument 1') - >>> print token.getarg(0) - argument 0 - >>> print token.getarg(1) - argument 1 - >>> print token.getarg(2) - None - >>> print token.getarg(-1) - argument 1 - >>> print token.getarg(-2) - argument 0 - >>> print token.getarg(-3) - None - ''' - return self.args[index] if index >= -len(self.args) and index < len(self.args) else None - def setarg(self, index, value=None): '''Sets argument at index, also verifies that the input contains no illegal characters. If the index argument is set but not value, then the index is assumed to be referring to @@ -358,7 +336,16 @@ def setarg(self, index, value=None): >>> print token [EXAMPLE:hi!:b:500]''' if value is None and index is not None: value = index; index = 0 - self.args[index] = rawstoken.sanitizeargstring(value) + self.args[index] = value + + def setargs(self, args=None): + if self.args is None: + self.args = tokenargs(args) + else: + self.args[:] = args + + def clearargs(self): + self.args.clear() def addarg(self, value): '''Appends an argument to the end of the argument list. @@ -373,10 +360,13 @@ def addarg(self, value): >>> print token [EXAMPLE:hi!] ''' - self.args.append(rawstoken.sanitizeargstring(value)) + self.args.append(value) + + def addargs(self, values): + self.args.extend(values) def containsarg(self, value): - return rawstoken.sanitizeargstring(value) in self.args + return value in self.args def argsstr(self): '''Return arguments joined by ':'. @@ -386,7 +376,7 @@ def argsstr(self): >>> print token.argsstr() a:b:c ''' - return ':'.join([str(a) for a in self.args]) + return str(self.args) def getvalue(self): '''Get the token's value. @@ -409,7 +399,7 @@ def setvalue(self, value): [JUST KIDDING:a:b:c] ''' valuestr = str(value) - if any([char in valuestr for char in rawstoken.illegal_internal_chars]): raise ValueError + if any([char in valuestr for char in rawstoken.illegal_internal_chars]): raise ValueError('Failed to set token value to %s because the string contains illegal characters.' % valuestr) self.value = value def getprefix(self): @@ -439,7 +429,7 @@ def setprefix(self, value): hello [EXAMPLE] ''' valuestr = str(value) - if any([char in valuestr for char in rawstoken.illegal_external_chars]): raise ValueError + if any([char in valuestr for char in rawstoken.illegal_external_chars]): raise ValueError('Failed to set token prefix to %s because the string contains illegal characters.' % valuestr) self.prefix = value def getsuffix(self): @@ -469,15 +459,28 @@ def setsuffix(self, value): [EXAMPLE] world ''' valuestr = str(value) - if any([char in valuestr for char in rawstoken.illegal_external_chars]): raise ValueError + if any([char in valuestr for char in rawstoken.illegal_external_chars]): raise ValueError('Failed to set token suffix to %s because the string contains illegal characters.' % valuestr) self.suffix = value - def arg(self): - '''When a token is expected to have only one argument, this method can be used - to access it. It there's one argument it will be returned, otherwise an - exception will be raised. + def arg(self, index=None): + '''When an index is given, the argument at that index is returned. If left + set to None then the first argument is returned if the token has exactly one + argument, otherwise an exception is raised. Example usage: + >>> token = raws.token('EXAMPLE:argument 0:argument 1') + >>> print token.getarg(0) + argument 0 + >>> print token.getarg(1) + argument 1 + >>> print token.getarg(2) + None + >>> print token.getarg(-1) + argument 1 + >>> print token.getarg(-2) + argument 0 + >>> print token.getarg(-3) + None >>> token_a = raws.token('EXAMPLE:x') >>> token_b = raws.token('EXAMPLE:x:y:z') >>> print token_a.arg() @@ -488,10 +491,11 @@ def arg(self): ... print 'token_b doesn\'t have the correct number of arguments!' ... token_b doesn't have the correct number of arguments!''' - if len(self.args) == 1: + if index is None: + if len(self.args) != 1: raise ValueError('Failed to retrieve token argument because it doesn\'t have exactly one.') return self.args[0] else: - raise ValueError + return self.args[index] def equals(self, other): '''Returns True if two tokens have identical values and arguments, False otherwise. @@ -513,12 +517,10 @@ def equals(self, other): >>> print token_c is token_a False ''' - return( other is not None and self.value == other.value and - self.nargs() == other.nargs() and - all([str(self.args[i]) == str(other.args[i]) for i in xrange(0, self.nargs())]) + self.args == other.args ) @staticmethod @@ -582,7 +584,7 @@ def copy(auto=None, token=None, tokens=None): prevtoken = newtoken return copied else: - raise ValueError + raise ValueError('Failed to copy token or tokens because no object was specified.') def tokens(self, range=None, include_self=False, reverse=False, until_token=None, step=None): '''Iterate through successive tokens starting with this one. @@ -623,29 +625,6 @@ def tokens(self, range=None, include_self=False, reverse=False, until_token=None if (step is None) or (count % step == 0): yield itertoken itertoken = itertoken.prev if reverse else itertoken.next count += 1 - - def match(self, filter=None, **kwargs): - '''Returns True if this method matches some filter, false otherwise. - - filter: The filter to check, for example a raws.tokenfilter object. - **kwargs: Passed to the taws.tokenfilter constructor to nab a filter - to use if no filter was otherwise specified. - - Example usage: - >>> filter = raws.tokenfilter(exact_value='EXAMPLE') - >>> token_a = raws.token('HELLO:THERE') - >>> token_b = raws.token('EXAMPLE') - >>> token_c = raws.token('EXAMPLE:NUMBER:TWO') - >>> print token_a.match(filter) - False - >>> print token_b.match(filter) - True - >>> print token_c.match(filter) - True - >>> print token_a.match(exact_value='HELLO') - True - ''' - return (filter if filter else rawstokenfilter(**kwargs)).match(self) def add(self, auto=None, pretty=None, token=None, tokens=None, reverse=False): '''Adds a token or tokens nearby this one. If reverse is False the token @@ -678,9 +657,9 @@ def add(self, auto=None, pretty=None, token=None, tokens=None, reverse=False): elif tokens is not None: return self.addall(tokens, reverse) else: - raise ValueError + raise ValueError('Failed to add token or tokens because no object was specified.') - def addprop(self, auto=None, **kwargs): + def addprop(self, *args, **kwargs): '''When this token is an object token like CREATURE:X or INORGANIC:X, a new token is usually added immediately afterwards. However, if a token like COPY_TAGS_FROM or USE_MATERIAL_TEMPLATE exists underneath the object, then @@ -710,7 +689,7 @@ def addprop(self, auto=None, **kwargs): aftervalues = ('COPY_TAGS_FROM',) addafter = self.getlastprop(value_in=aftervalues) if not addafter: addafter = self - addafter.add(auto=auto, **kwargs) + addafter.add(*args, **kwargs) @staticmethod def firstandlast(tokens, setfile=None): @@ -724,7 +703,7 @@ def firstandlast(tokens, setfile=None): try: if setfile is not None: raise ValueError return tokens[0], tokens[-1] - except: + except Exception as e: first, last = None, None for token in tokens: if first is None: first = token @@ -759,7 +738,7 @@ def addall(self, tokens, reverse=False): else: first.prev = self last.next = self.next - if self.next: self.next.prev = tokens[-1] + if self.next: self.next.prev = last self.next = first return tokens @@ -836,7 +815,7 @@ def parse(data, implicit_braces=True, **kwargs): tokens.append(token) return tokens else: - raise ValueError + raise ValueError('Failed to parse data string because it had no braces and because implicit_braces was set to False.') else: while pos < len(data): token = None @@ -879,5 +858,5 @@ def parseone(*args, **kwargs): There was more than one token! ''' tokens = rawstoken.parse(*args, **kwargs) - if len(tokens) != 1: raise ValueError + if len(tokens) != 1: raise ValueError('Failed to parse one token because the data string contained %d tokens.' % len(tokens)) return tokens[0] diff --git a/raws/tokenargs.py b/raws/tokenargs.py new file mode 100644 index 0000000..b8325b7 --- /dev/null +++ b/raws/tokenargs.py @@ -0,0 +1,68 @@ +class tokenargs(list): + + # Disallow these characters inside token arguments + illegal = '[]:' + + # Replace simple inputs with illegal characters with their legal equivalents + replace = { + "':'": '58', + "'['": '91', + "']'": '93' + } + + @staticmethod + def sanitize(value): + '''Internal: Utility method for sanitizing a string intended to be evaluated as an arugment for a token.''' + valuestr = str(value) + if valuestr in tokenargs.replace: + valuestr = tokenargs.replace[valuestr] + else: + if any([char in valuestr for char in tokenargs.illegal]): raise ValueError('Illegal character in argument %s.' % valuestr) + return valuestr + + def append(self, item): + list.append(self, tokenargs.sanitize(item)) + + def extend(self, items): + list.extend(self, (tokenargs.sanitize(item) for item in items)) + + def insert(self, index, item): + list.insert(self, index, tokenargs.sanitize(item)) + + def clear(self): + del self[:] + + def reset(self, items): + self[:] = (tokenargs.sanitize(item) for item in items) + + def __str__(self): + return ':'.join(self) + + def __repr__(self): + return str(self) + + def __add__(self, items): + return list.__add__(self, (tokenargs.sanitize(item) for item in items)) + + def __contains__(self, item): + try: + san = tokenargs.sanitize(item) + except: + return False + else: + return list.__contains__(self, san) + + def __iadd__(self, item): + list.__iadd__(self, tokenargs.sanitize(item)) + + def __init__(self, items=None): + if items: + list.__init__(self, (tokenargs.sanitize(item) for item in items)) + else: + list.__init__(self) + + def __setslice__(self, start, stop, items): + list.__setslice__(self, start, stop, (tokenargs.sanitize(item) for item in items)) + + def __setitem__(self, index, item): + list.__setitem__(self, index, tokenargs.sanitize(item)) diff --git a/readme.md b/readme.md index 63eb5fa..c083b4c 100644 --- a/readme.md +++ b/readme.md @@ -1,13 +1,17 @@ # PyDwarf +I have an enormous love for Dwarf Fortress and for its mods. In the years I've been playing and following this game, I think that there's always been a gap where a really powerful raws mod manager could be. This is the niche PyDwarf is intended to fill: I want to give mod authors the power to access and modify Dwarf Fortress's files with an elegance hitherto unseen, and I want to give users the ability to apply these mods with a minimum of effort. + PyDwarf is licensed via the exceptionally permissive [zlib/libpng license](https://github.com/pineapplemachine/PyDwarf/blob/master/license.txt). It's written for [Python 2.7](https://www.python.org/download/releases/2.7.8/) and is intended as a mod tool for the wonderful game [Dwarf Fortress](http://www.bay12games.com/dwarves/). -I have an enormous love for Dwarf Fortress and for its mods. In the years I've been playing and following this game, I think that there's always been a gap where a really powerful raws mod manager could be. This is the niche PyDwarf is intended to fill: I want to give mod authors the power to query and modify Dwarf Fortress's raws files with an elegance hitherto unseen, and I want to give users the ability to apply these mods with a minimum of effort. +## Configuring PyDwarf + +PyDwarf is easy to configure! Open `config.json` with your favorite plain text editor and tell it where to find your Dwarf Fortress raws, edit the list of scripts to reflect the mods you want to run and in what order you'd like them to run. Once the configuration is to your liking simply run `manager.py`, with Python 2.7 installed this can be done by opening a command line in the PyDwarf directory and running `python manager.py`. -PyDwarf is easy to configure! Open `config.json` with your favorite text editor and tell it where to find your Dwarf Fortress raws, edit the list of scripts to reflect the mods you want to run and in what order you'd like them to run. Once the configuration is to your liking simply run `manager.py`, with Python 2.7 installed this can be done by opening a command line in the PyDwarf directory and running `python manager.py`. +Here's a [step-by-step tutorial](docs/configuring.md) describing what this process might entail, and here's a full list of the scripts which [come bundled with PyDwarf](docs/scripts.md). -If you're interested in writing your own mods for PyDwarf or in understanding its more advanced features, take a look at [the tutorial](https://github.com/pineapplemachine/PyDwarf/blob/master/tutorial.md)! +## Modding using PyDwarf -You can see a list of your installed mods by running `python manager.py --list` and you can view some documentation for a particular script by running, for example, `python manager.py --meta "pineapple.flybears"`. +If you're interested in writing your own mods for PyDwarf or in understanding its more advanced features, take a look at [this tutorial](docs/tutorial.md), which goes into detail about how to use PyDwarf and how to write mods using it. -![Image of a flying female bear](https://github.com/pineapplemachine/PyDwarf/blob/master/images/logo_transparent.png?raw=true) +![Image of a flying female bear](images/logo_transparent.png) diff --git a/scripts/__init__.py b/scripts/__init__.py index 4154d7c..fe9950a 100644 --- a/scripts/__init__.py +++ b/scripts/__init__.py @@ -1,24 +1,36 @@ -# Imports all scripts in this directory - -import sys -sys.path.append('../') - -import os -import imp -import pydwarf - -__all__ = [] - -for root, dirs, files in os.walk(os.path.dirname(os.path.realpath(__file__))): - for filename in files: - path = os.path.join(root, filename) - modulename = '.'.join(os.path.basename(filename).split('.')[1:-1]) - if filename.endswith('.py') and filename.startswith('pydwarf.'): - pydwarf.log.debug('Loading script %s from %s...' % (modulename, path)) - try: - with open(path, 'U') as modulefile: - module = imp.load_module(modulename, modulefile, path, ('.py', 'U', imp.PY_SOURCE)) - globals()[modulename] = module - __all__.append(modulename) - except: - pydwarf.log.exception('Failed to load script from %s' % path) +#!/usr/bin/env python + +__author__ = 'Sophie Kirschner' +__license__ = 'zlib/libpng' +__email__ = 'sophiek@pineapplemachine.com' +__version__ = '1.0.1' + + + +'''Recursively imports every PyDwarf script in this directory.''' + + + +import sys +sys.path.append('../') + +import os +import imp +import pydwarf + + + +__all__ = [] + +for root, dirs, files in os.walk(os.path.dirname(os.path.realpath(__file__))): + for filename in files: + path = os.path.join(root, filename) + modulename = '.'.join(os.path.basename(filename).split('.')[1:-1]) + if filename.endswith('.py') and filename.startswith('pydwarf.'): + try: + with open(path, 'U') as modulefile: + module = imp.load_module(modulename, modulefile, path, ('.py', 'U', imp.PY_SOURCE)) + globals()[modulename] = module + __all__.append(modulename) + except: + pydwarf.log.exception('Failed to load script from %s' % path) diff --git a/scripts/omniclasm/hack/decay/deteriorateclothes.rb b/scripts/omniclasm/hack/decay/deteriorateclothes.rb new file mode 100644 index 0000000..dc61965 --- /dev/null +++ b/scripts/omniclasm/hack/decay/deteriorateclothes.rb @@ -0,0 +1,66 @@ +class DeteriorateClothes + + def initialize + end + + def process + return false unless @running + + items = [df.world.items.other[:GLOVES], + df.world.items.other[:ARMOR], + df.world.items.other[:SHOES], + df.world.items.other[:PANTS], + df.world.items.other[:HELM]] + + items.each { |type| + type.each { |i| + if (i.subtype.armorlevel == 0 and i.flags.on_ground == true and i.wear > 0) + i.wear_timer *= i.wear + 0.5 + if (i.wear > 2) + i.flags.garbage_collect = true + end + end + } + } + + + end + + def start + @onupdate = df.onupdate_register('deteriorateclothes', 1200, 1200) { process } + @running = true + + puts "Deterioration of old clothes commencing..." + + end + + def stop + df.onupdate_unregister(@onupdate) + @running = false + end + + def status + @running ? 'Running.' : 'Stopped.' + end + +end + +case $script_args[0] +when 'start' + if ($DeteriorateClothes) + $DeteriorateClothes.stop + end + + $DeteriorateClothes = DeteriorateClothes.new + $DeteriorateClothes.start + +when 'end', 'stop' + $DeteriorateClothes.stop + +else + if $DeteriorateClothes + puts $DeteriorateClothes.status + else + puts 'Not loaded.' + end +end \ No newline at end of file diff --git a/scripts/omniclasm/hack/decay/deterioratecorpses.rb b/scripts/omniclasm/hack/decay/deterioratecorpses.rb new file mode 100644 index 0000000..982dc02 --- /dev/null +++ b/scripts/omniclasm/hack/decay/deterioratecorpses.rb @@ -0,0 +1,78 @@ +class DeteriorateCorpses + + def initialize + end + + def process + return false unless @running + + df.world.items.other[:ANY_CORPSE].each { |i| + if (i.flags.dead_dwarf == false) + i.wear_timer += 1 + if (i.wear_timer > 24 + rand(8)) + i.wear_timer = 0 + i.wear += 1 + end + if (i.wear > 3) + i.flags.garbage_collect = true + end + + end + + } + + df.world.items.other[:REMAINS].each { |i| + if (i.flags.dead_dwarf == false) + i.wear_timer += 1 + if (i.wear_timer > 6) + i.wear_timer = 0 + i.wear += 1 + end + if (i.wear > 3) + i.flags.garbage_collect = true + end + + end + + } + + end + + def start + @onupdate = df.onupdate_register('deterioratecorpses', 1200, 1200) { process } + @running = true + + puts "Deterioration of body parts commencing..." + + end + + def stop + df.onupdate_unregister(@onupdate) + @running = false + end + + def status + @running ? 'Running.' : 'Stopped.' + end + +end + +case $script_args[0] +when 'start' + if ($DeteriorateCorpses) + $DeteriorateCorpses.stop + end + + $DeteriorateCorpses = DeteriorateCorpses.new + $DeteriorateCorpses.start + +when 'end', 'stop' + $DeteriorateCorpses.stop + +else + if $DeteriorateCorpses + puts $DeteriorateCorpses.status + else + puts 'Not loaded.' + end +end \ No newline at end of file diff --git a/scripts/omniclasm/hack/decay/deterioratefood.rb b/scripts/omniclasm/hack/decay/deterioratefood.rb new file mode 100644 index 0000000..697adc0 --- /dev/null +++ b/scripts/omniclasm/hack/decay/deterioratefood.rb @@ -0,0 +1,70 @@ +class DeteriorateFood + + def initialize + end + + def process + return false unless @running + + items = [df.world.items.other[:FISH], + df.world.items.other[:FISH_RAW], + df.world.items.other[:EGG], + df.world.items.other[:CHEESE], + df.world.items.other[:PLANT], + df.world.items.other[:PLANT_GROWTH], + df.world.items.other[:FOOD]] + + items.each { |type| + type.each { |i| + i.wear_timer += 1 + if (i.wear_timer > 24 + rand(8)) + i.wear_timer = 0 + i.wear += 1 + end + if (i.wear > 3) + i.flags.garbage_collect = true + end + } + } + + + end + + def start + @onupdate = df.onupdate_register('deterioratefood', 1200, 1200) { process } + @running = true + + puts "Deterioration of food commencing..." + + end + + def stop + df.onupdate_unregister(@onupdate) + @running = false + end + + def status + @running ? 'Running.' : 'Stopped.' + end + +end + +case $script_args[0] +when 'start' + if ($DeteriorateFood) + $DeteriorateFood.stop + end + + $DeteriorateFood = DeteriorateFood.new + $DeteriorateFood.start + +when 'end', 'stop' + $DeteriorateFood.stop + +else + if $DeteriorateFood + puts $DeteriorateFood.status + else + puts 'Not loaded.' + end +end \ No newline at end of file diff --git a/scripts/omniclasm/hack/decay/starvingdead.rb b/scripts/omniclasm/hack/decay/starvingdead.rb new file mode 100644 index 0000000..193628d --- /dev/null +++ b/scripts/omniclasm/hack/decay/starvingdead.rb @@ -0,0 +1,84 @@ +class StarvingDead + + def initialize + @threshold = 1 + @die_threshold = 3 + end + + def process + return false unless @running + + month_length = 67200 + + if (@undead_count >= 25) + month_length *= 25 / @undead_count + end + + @undead_count = 0 + df.world.units.active.each { |u| + if (u.enemy.undead and not u.flags1.dead) + @undead_count += 1 + if (u.curse.time_on_site > month_length * @threshold) + u.body.physical_attrs.each { |att| + att.value = att.value - (att.value * 0.02) + } + end + + if (u.curse.time_on_site > month_length * @die_threshold) + u.flags1.dead = true + u.curse.rem_tags2.FIT_FOR_ANIMATION = true + end + + end + + } + + end + + def start + @onupdate = df.onupdate_register('starvingdead', 1200, 1200) { process } + @running = true + @undead_count = 0 + + if ($script_args[1] and $script_args[1].gsub(/[^0-9\.]/,'').to_f > 0) + @threshold = $script_args[1].gsub(/[^0-9\.]/,'').to_f + end + + if ($script_args[2] and $script_args[2].gsub(/[^0-9\.]/,'').to_f > 0) + @die_threshold = $script_args[2].gsub(/[^0-9\.]/,'').to_f + end + + puts "Starving Dead starting...weakness starts at #{@threshold} months, true death at #{@die_threshold} months" + end + + def stop + df.onupdate_unregister(@onupdate) + @running = false + end + + def status + @running ? 'Running.' : 'Stopped.' + end + +end + +case $script_args[0] +when 'start' + + if ($StarvingDead) + $StarvingDead.stop + end + + $StarvingDead = StarvingDead.new + $StarvingDead.start + +when 'end', 'stop' + $StarvingDead.stop + +else + if $StarvingDead + puts $StarvingDead.status + else + puts 'Not loaded.' + end +end \ No newline at end of file diff --git a/scripts/omniclasm/pydwarf.decay.py b/scripts/omniclasm/pydwarf.decay.py new file mode 100644 index 0000000..53a717b --- /dev/null +++ b/scripts/omniclasm/pydwarf.decay.py @@ -0,0 +1,107 @@ +import pydwarf +import raws + + + +decay_dir = pydwarf.rel(__file__, 'hack/decay') + + + +@pydwarf.urist( + name = 'omniclasm.decay.starvingdead', + version = '1.0.0', + author = ('Omniclasm', 'Sophie Kirschner'), + description = '''With this script running, all undead that have been on the map for a + time (default: 1 month) start to gradually decay, losing strength, speed, and + toughness. After they have been on the map for even longer (default: 3 months), + they collapse upon themselves, never to be reanimated. + ''', + arguments = { + 'start': 'Number of months before decay sets in.', + 'die': 'Number of months before collapsing entirely.', + 'auto_run': 'If set to True then the script will be started automatically upon startup.' + }, + compatibility = (pydwarf.df_0_40, pydwarf.df_0_3x) +) +def starvingdead(df, start=1, die=3, auto_run=True): + return pydwarf.urist.getfn('pineapple.utils.addhack')( + df, + auto_run = 'starvingdead start %d %d' % (start, die) if auto_run is True else auto_run, + loc = 'hack/scripts', + path = pydwarf.rel(decay_dir, 'starvingdead.rb'), + kind = raws.reffile + ) + + + +@pydwarf.urist( + name = 'omniclasm.decay.deteriorate.corpses', + version = '1.0.0', + author = ('Omniclasm', 'Sophie Kirschner'), + description = '''In long running forts, especially evil biomes, you end up with a lot of + toes, teeth, fingers, and limbs scattered all over the place. Various corpses from + various sieges, stray kitten corpses, probably some heads. This script causes all of + those to rot away into nothing after several months. + ''', + arguments = { + 'auto_run': 'If set to True then the script will be started automatically upon startup.' + }, + compatibility = (pydwarf.df_0_40, pydwarf.df_0_3x) +) +def deterioratecorpses(df, auto_run=True): + return pydwarf.urist.getfn('pineapple.utils.addhack')( + df, + auto_run = 'deterioratecorpses start' if auto_run is True else auto_run, + loc = 'hack/scripts', + path = pydwarf.rel(decay_dir, 'deterioratecorpses.rb'), + kind = raws.reffile + ) + + + +@pydwarf.urist( + name = 'omniclasm.decay.deteriorate.clothes', + version = '1.0.0', + author = ('Omniclasm', 'Sophie Kirschner'), + description = '''This script is fairly straight forward. All of those slightly worn wool + shoes that dwarves scatter all over the place will deteriorate at a greatly increased + rate, and eventually just crumble into nothing. As warm and fuzzy as a dining room + full of used socks makes your dwarves feel, your FPS does not like it. + ''', + arguments = { + 'auto_run': 'If set to True then the script will be started automatically upon startup.' + }, + compatibility = (pydwarf.df_0_40, pydwarf.df_0_3x) +) +def deteriorateclothes(df, auto_run=True): + return pydwarf.urist.getfn('pineapple.utils.addhack')( + df, + auto_run = 'deteriorateclothes start' if auto_run is True else auto_run, + loc = 'hack/scripts', + path = pydwarf.rel(decay_dir, 'deteriorateclothes.rb'), + kind = raws.reffile + ) + + + +@pydwarf.urist( + name = 'omniclasm.decay.deteriorate.food', + version = '1.0.0', + author = ('Omniclasm', 'Sophie Kirschner'), + description = '''With this script running, all food and plants wear out and disappear after + several months. Barrels and stockpiles will keep them from rotting, but it won't keep + them from decaying. + ''', + arguments = { + 'auto_run': 'If set to True then the script will be started automatically upon startup.' + }, + compatibility = (pydwarf.df_0_40, pydwarf.df_0_3x) +) +def deterioratefood(df, auto_run=True): + return pydwarf.urist.getfn('pineapple.utils.addhack')( + df, + auto_run = 'deterioratefood start' if auto_run is True else auto_run, + loc = 'hack/scripts', + path = pydwarf.rel(decay_dir, 'deterioratefood.rb'), + kind = raws.reffile + ) diff --git a/scripts/omniclasm/readme.md b/scripts/omniclasm/readme.md new file mode 100644 index 0000000..71de172 --- /dev/null +++ b/scripts/omniclasm/readme.md @@ -0,0 +1,56 @@ +# State of Decay + +Created by Omniclasm. (http://www.bay12forums.com/smf/index.php?topic=150782.0) +Rewritten for PyDwarf by Sophie Kirschner. (http://www.pineapplemachine.com) + +Collection of DFHack scripts that are somewhere between a "mod" and a "fps booster". Fully modular, you can pick and choose which scripts you want or don't want. + +## starvingdead + +This was the original script, and probably has the least impact on vanilla gameplay. It mostly helps prevent undead cascades in the caverns. What is an undead cascade? An undead cascade is when a forgotten beast kills a (living) troglodyte, then the now reanimated troglodyte kills the forgotten beast, then they both proceed to kill every living thing in the cavern. Then every previously living thing reanimates. Then they kill every other living thing. Then those reanimate as well, and next thing you know, you have 300+ undead roaming the caverns and destroying your FPS. + +So, how does this script prevent that? With this script running, all undead that have been on the map for a time (default: 1 month) start to gradually decay, losing strength, speed, and toughness. After they have been on the map for even longer (default: 3 months), they collapse upon themselves, never to be reanimated. + +Usage: +``` shell +starvingdead start +starvingdead stop +``` + +## deteriorate + +These modules have a general concept of "use it or lose it". If something isn't being used, it quickly wears away until it no longer exists, freeing up some valuable FPS. + +### corpses + +In long running forts, especially evil biomes, you end up with a lot of toes, teeth, fingers, and limbs scattered all over the place. Various corpses from various sieges, stray kitten corpses, probably some heads. Basically, your map will look like a giant pile of assorted body parts, all of which individually eat up a small part of your FPS, which collectively eat up quite a bit. + +In addition, this script also targets various butchery byproducts. Enjoying your thriving animal industry? Your FPS does not. Those thousands of skulls, bones, hooves, and wool eat up precious FPS that could be used to kill goblins and elves. Whose corpses will also get destroyed by the script to kill more goblins and elves. + +This script causes all of those to rot away into nothing after several months. + +Usage: +``` shell +deterioratecorpses start +deterioratecorpses stop +``` + +### clothes + +This script is fairly straight forward. All of those slightly worn wool shoes that dwarves scatter all over the place will deteriorate at a greatly increased rate, and eventually just crumble into nothing. As warm and fuzzy as a dining room full of used socks makes your dwarves feel, your FPS does not like it. + +Usage: +``` shell +deteriorateclothes start +deteriorateclothes stop +``` + +### food + +This script is...pretty far reaching. However, almost all long running forts I've had end up sitting on thousands and thousands of food items. Several thousand cooked meals, three thousand plump helmets, just as many fish and meat. It gets pretty absurd. And your FPS doesn't like it. With this script running, all food and plants wear out and disappear after several months. Barrels and stockpiles will keep them from rotting, but it won't keep them from decaying. No more sitting on a hundred years worth of food. No more keeping barrels of pig tails sitting around until you decide to use them. Either use it, eat it, or lose it. Seeds, are excluded from this, if you aren't planning on using your pig tails, hold onto the seeds for a rainy day. + +Usage: +``` shell +deterioratefood start +deterioratefood stop +``` diff --git a/scripts/pineapple/pydwarf.bauxitetoaluminum.py b/scripts/pineapple/pydwarf.bauxitetoaluminum.py index 66fbb30..c5d5868 100644 --- a/scripts/pineapple/pydwarf.bauxitetoaluminum.py +++ b/scripts/pineapple/pydwarf.bauxitetoaluminum.py @@ -2,24 +2,15 @@ -alumtobaux_reaction = ''' - [NAME:smelt aluminum from bauxite] - [SMELTER] - [REAGENT:1:STONE:NO_SUBTYPE:STONE:BAUXITE] - [REAGENT:1:STONE:NO_SUBTYPE:STONE:CRYOLITE] - [PRODUCT:100:1:BAR:NO_SUBTYPE:METAL:ALUMINUM] - [FUEL] -''' - default_entities = ['MOUNTAIN'] -default_file = 'reaction_smelter_bauxtoalum_pineapple' +default_file = 'raw/objects/reaction_smelter_bauxtoalum_pineapple.txt' @pydwarf.urist( name = 'pineapple.bauxitetoaluminum', - version = '1.0.0', + version = '1.0.1', author = 'Sophie Kirschner', description = '''Adds a reaction to the smelter to allow the creation of aluminum bars from bauxite (as ore) and cryolite (as flux). Credit to this forum discussion for the @@ -34,21 +25,31 @@ }, compatibility = (pydwarf.df_0_3x, pydwarf.df_0_40) ) -def bauxtoalum(df, aluminum_value=0.75, entities=default_entities, add_to_file=default_file): +def bauxitetoaluminum(df, aluminum_value=0.75, entities=default_entities, add_to_file=default_file): # Affect value of aluminum pydwarf.log.debug('Multiplying value of aluminum by %f.' % aluminum_value) try: - matvaluetoken = df.getobj('INORGANIC:ALUMINUM').getprop('MATERIAL_VALUE') + aluminum = df.getobj('INORGANIC:ALUMINUM') + if aluminum is None: return pydwarf.failure('Couldn\'t find aluminum token to affect its value.') + matvaluetoken = aluminum.getprop('MATERIAL_VALUE') matvaluetoken.args[0] = str( float(matvaluetoken.args[0]) * aluminum_value ) except: pydwarf.log.exception('Failed to affect value of aluminum.') return pydwarf.failure() # Add the reaction - return pydwarf.urist.getfn('pineapple.utils.addreaction')( + return pydwarf.urist.getfn('pineapple.utils.addobject')( df, + type = 'REACTION', id = 'SMELT_BAUXITE_ALUMINUM_PINEAPPLE', - tokens = alumtobaux_reaction, + tokens = ''' + [NAME:smelt aluminum from bauxite] + [SMELTER] + [REAGENT:1:STONE:NO_SUBTYPE:STONE:BAUXITE] + [REAGENT:1:STONE:NO_SUBTYPE:STONE:CRYOLITE] + [PRODUCT:100:1:BAR:NO_SUBTYPE:METAL:ALUMINUM] + [FUEL] + ''', add_to_file = add_to_file, permit_entities = entities ) diff --git a/scripts/pineapple/pydwarf.boneflux.py b/scripts/pineapple/pydwarf.boneflux.py index 1785222..efdb57b 100644 --- a/scripts/pineapple/pydwarf.boneflux.py +++ b/scripts/pineapple/pydwarf.boneflux.py @@ -2,17 +2,6 @@ -boneflux_reaction = ''' - [NAME:%(name)s] - [BUILDING:KILN:NONE] - [REAGENT:bone:%(bones)s:NONE:NONE:NONE:NONE] - [USE_BODY_COMPONENT] - [ANY_BONE_MATERIAL] - [PRODUCT:100:1:BOULDER:NONE:INORGANIC:%(product)s] - [FUEL] - [SKILL:SMELT] -''' - default_product_id = 'CALCITE' default_reaction_name = 'make calcite from bones' @@ -21,13 +10,13 @@ default_entities = ['MOUNTAIN', 'PLAINS'] -default_file = 'reaction_kiln_boneflux_pineapple' +default_file = 'raw/objects/reaction_kiln_boneflux_pineapple.txt' @pydwarf.urist( name = 'pineapple.boneflux', - version = '1.0.1', + version = '1.0.2', author = 'Sophie Kirschner', description = '''Adds a reaction to the kiln which consumes bones and produces flux. Inspired by/stolen from Rubble's Bone Flux mod.''', @@ -41,10 +30,20 @@ compatibility = (pydwarf.df_0_2x, pydwarf.df_0_3x, pydwarf.df_0_40) ) def boneflux(df, product_id=default_product_id, reaction_name=default_reaction_name, bone_count=default_bone_count, entities=default_entities, add_to_file=default_file): - return pydwarf.urist.getfn('pineapple.utils.addreaction')( + return pydwarf.urist.getfn('pineapple.utils.addobject')( df, + type = 'REACTION', id = 'BONE_TO_FLUX_PINEAPPLE', - tokens = boneflux_reaction % { + tokens = ''' + [NAME:%(name)s] + [BUILDING:KILN:NONE] + [REAGENT:bone:%(bones)s:NONE:NONE:NONE:NONE] + [USE_BODY_COMPONENT] + [ANY_BONE_MATERIAL] + [PRODUCT:100:1:BOULDER:NONE:INORGANIC:%(product)s] + [FUEL] + [SKILL:SMELT] + ''' % { 'name': reaction_name, 'product': product_id, 'bones': bone_count diff --git a/scripts/pineapple/pydwarf.castanvil.py b/scripts/pineapple/pydwarf.castanvil.py index cbd1ce8..1558dba 100644 --- a/scripts/pineapple/pydwarf.castanvil.py +++ b/scripts/pineapple/pydwarf.castanvil.py @@ -2,26 +2,17 @@ -cast_anvil_reaction = ''' - [NAME:cast iron anvil] - [BUILDING:SMELTER:NONE] - [REAGENT:A:%d:BAR:NO_SUBTYPE:METAL:IRON] - [PRODUCT:100:1:ANVIL:NONE:METAL:IRON] - [FUEL] - [SKILL:SMELT] -''' - -default_entities = ['MOUNTAIN'] +default_entities = 'MOUNTAIN' default_anvil_cost = 5 # Cost in iron bars -default_file = 'reaction_smelter_castanvil_pineapple' +default_file = 'raw/objects/reaction_smelter_castanvil_pineapple.txt' @pydwarf.urist( name = 'pineapple.castanvil', - version = '1.0.0', + version = '1.0.1', author = 'Sophie Kirschner', description = '''Adds a reaction to the smelter which makes it possible to create an iron anvil without already having a forge. @@ -35,10 +26,18 @@ ) def castanvil(df, anvil_cost=default_anvil_cost, entities=default_entities, add_to_file=default_file): # Super easy using pineapple.utils - return pydwarf.urist.getfn('pineapple.utils.addreaction')( + return pydwarf.urist.getfn('pineapple.utils.addobject')( df, + type = 'REACTION', id = 'CAST_IRON_ANVIL_PINEAPPLE', - tokens = cast_anvil_reaction % (anvil_cost * 150), + tokens = ''' + [NAME:cast iron anvil] + [BUILDING:SMELTER:NONE] + [REAGENT:A:%d:BAR:NO_SUBTYPE:METAL:IRON] + [PRODUCT:100:1:ANVIL:NONE:METAL:IRON] + [FUEL] + [SKILL:SMELT] + ''' % (anvil_cost * 150), add_to_file = add_to_file, permit_entities = entities ) diff --git a/scripts/pineapple/pydwarf.cavegrass.py b/scripts/pineapple/pydwarf.cavegrass.py index d1782a2..ef609b6 100644 --- a/scripts/pineapple/pydwarf.cavegrass.py +++ b/scripts/pineapple/pydwarf.cavegrass.py @@ -103,7 +103,7 @@ @pydwarf.urist( name = 'pineapple.cavegrass', - version = '1.0.1', + version = '1.0.2', author = 'Sophie Kirschner', description = '''Changes the grasses in each cavern level to make the different levels more visually distinct, as well as adding a much greater variety. @@ -120,9 +120,12 @@ def cavegrass(df, grasses=default_grasses, add_file='plant_grasses_cavegrass_pin # Add the new file for new grasses grassfile = None if add_file: - if add_file in df.files: return pydwarf.failure('File %s already exists.' % add_file) - grassfile = df.addfile(filename=add_file) - grassfile.add('OBJECT:PLANT') + try: + grassfile = df.add(add_file) + grassfile.add('OBJECT:PLANT') + except: + pydwarf.log.exception('Failed to add file %s.' % add_file) + return pydwarf.failure('Failed to add file %s.' % add_file) # Handle each grass failures = 0 diff --git a/scripts/pineapple/pydwarf.diff.py b/scripts/pineapple/pydwarf.diff.py index d768ff6..4684b96 100644 --- a/scripts/pineapple/pydwarf.diff.py +++ b/scripts/pineapple/pydwarf.diff.py @@ -29,7 +29,7 @@ def __str__(self): @pydwarf.urist( name = 'pineapple.diff', - version = '1.0.0', + version = '1.0.1', author = 'Sophie Kirschner', description = '''Merges and applies changes made to some modded raws via diff checking. Should be reasonably smart about automatic conflict resolution but if it complains @@ -44,14 +44,14 @@ def __str__(self): }, compatibility = '.*' ) -def diff(dfraws, paths): +def diff(df, paths): # Get all the files in the mods newfiles = [] for path in paths: if os.path.isfile(path) and path.endswith('.txt'): with open(path, 'rb') as rfilestream: - rfiles = (raws.file(rfile=rfilestream, path=path),) + rfiles = (raws.rawfile(rfile=rfilestream, path=path),) elif os.path.isdir(path): rfiles = raws.dir(path=path).files.values() else: @@ -70,8 +70,8 @@ def diff(dfraws, paths): currentfiletokens = None if newfile.header in currentfiletokensdict: currentfiletokens = currentfiletokensdict[newfile.header] - elif newfile.header in dfraws.files: - currentfiletokens = list(dfraws.getfile(newfile.header)) + elif newfile.header in df.files: + currentfiletokens = list(df.getfile(newfile.header)) currentfiletokensdict[newfile.header] = currentfiletokens # Do a diff @@ -87,7 +87,7 @@ def diff(dfraws, paths): # File doesn't exist yet, don't bother with a diff else: pydwarf.log.debug('File didn\'t exist yet, adding...') - dfraws.addfile(rfile=newfile) + df.add(newfile) for fileheader, fileops in operations.iteritems(): # Do some handling for potentially conflicting replacements diff --git a/scripts/pineapple/pydwarf.easypatch.py b/scripts/pineapple/pydwarf.easypatch.py new file mode 100644 index 0000000..a3c5f61 --- /dev/null +++ b/scripts/pineapple/pydwarf.easypatch.py @@ -0,0 +1,75 @@ +import os + +import pydwarf +import raws + + + +@pydwarf.urist( + name = 'pineapple.easypatch', + version = '1.0.0', + author = 'Sophie Kirschner', + description = '''Given a path to a file, a directory, a content string, a tokenlist, a raws + file object, or an iterable containing a combination of these, a file or files are added + to the dir object, and these same objects can be permitted using the permitted_entities + argument.''', + arguments = { + 'files': '''The file or files to be added.''', + '**kwargs': 'Passed on to pineapple.utils.permitobjects.', + }, + compatibility = '.*' +) +def easypatch(df, files, **kwargs): + if isinstance(files, basestring): + if os.path.isfile(files): + return easypatch_filepath(df, files, **kwargs) + elif os.path.isdir(files): + return easypatch_dirpath(df, files, collision_fails=False, **kwargs) + else: + return easypatch_content(df, files, **kwargs) + elif isinstance(files, raws.tokenlist): + return easypatch_tokens(df, files, **kwargs) + elif isinstance(files, raws.rawfile): + return easypatch_file(df, files, **kwargs) + else: + for file in files: + response = easypatch(df, file, collision_fails=False, **kwargs) + if not response: return response + return pydwarf.success('Added %d files.' % len(files)) + + + +def easypatch_dirpath(df, path, loc=None, **kwargs): + for root, dirnames, filenames in os.walk(path): + for filename in filenames: + filepath = os.path.join(root, filename) + response = easypatch_filepath(df, path=filepath, loc=loc, root=root, **kwargs) + if not response: return response + return pydwarf.success('Added files from directory %s.' % path) + +def easypatch_filepath(df, path, loc=None, root=None, **kwargs): + file = raws.rawfile(path=path, loc=loc, root=root) + return easypatch_file(df, file, **kwargs) + +def easypatch_content(df, content, loc, **kwargs): + file = raws.rawfile(path=loc, content=content) + return easypatch_file(df, file, **kwargs) + +def easypatch_tokens(df, tokens, loc, **kwargs): + file = raws.rawfile(path=loc, tokens=tokens) + return easypatch_file(df, file, **kwargs) + +def easypatch_file(df, file, collision_fails=True, replace=False, **kwargs): + if replace or str(file) not in df: + df.add(file, replace=replace) + objects = file.allobj() + response = pydwarf.urist.getfn('pineapple.utils.permitobjects')( + df, + objects = objects, + **kwargs + ) + return response + elif collision_fails: + return pydwarf.failure('Failed to add file because a file by the same name already exists in the dir.') + else: + return pydwarf.success('Didn\'t add the file because a file by the same name already exists in the dir.') diff --git a/scripts/pineapple/pydwarf.greensteel.py b/scripts/pineapple/pydwarf.greensteel.py index c33d612..4b8b0fb 100644 --- a/scripts/pineapple/pydwarf.greensteel.py +++ b/scripts/pineapple/pydwarf.greensteel.py @@ -1,15 +1,17 @@ -import os import pydwarf +import raws -greendir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'raws/greensteel') -default_entities = ['MOUNTAIN'] +greendir = pydwarf.rel(__file__, 'raw/greensteel') + +default_entities = 'MOUNTAIN' + @pydwarf.urist( name = 'pineapple.greensteel', - version = '1.0.0', + version = '1.0.1', author = 'Sophie Kirschner', description = '''Adds an alloy which is lighter and sharper than steel but not so much as adamantine. It can be made from similar ingredients as steel with the addition @@ -22,16 +24,9 @@ ) def greensteel(df, entities=default_entities): # Add greensteel raws - try: - df.read(path=greendir, log=pydwarf.log) - return pydwarf.urist.getfn('pineapple.utils.addtoentity')( - df, - entities = entities, - permitted_reaction = ( - 'GREEN_STEEL_MAKING_ADAMANT_PINEAPPLE', - 'GREEN_STEEL_MAKING_ADMANTINE_PINEAPPLE' - ) - ) - except: - pydwarf.log.exception('Failed to add greensteel raws.') - return pydwarf.failure() + return pydwarf.urist.getfn('pineapple.easypatch')( + df, + files = greendir, + loc = 'raw/objects', + permit_entities = entities + ) diff --git a/scripts/pineapple/pydwarf.metalitems.py b/scripts/pineapple/pydwarf.metalitems.py index 962627b..1ae872e 100644 --- a/scripts/pineapple/pydwarf.metalitems.py +++ b/scripts/pineapple/pydwarf.metalitems.py @@ -1,4 +1,5 @@ import pydwarf +import raws @@ -21,7 +22,7 @@ @pydwarf.urist( name = 'pineapple.metalitems', - version = '1.0.0', + version = '1.0.1', author = 'Sophie Kirschner', description = 'Allows the forging of every type of metal item from the specified metals.', arguments = { @@ -31,29 +32,19 @@ compatibility = (pydwarf.df_0_3x, pydwarf.df_0_40) ) def metalitems(df, metals=default_metals, items=default_item_tokens): - # Handle each metal - modified = 0 - for inorganictoken in df.allobj('INORGANIC'): - if inorganictoken.args[0] in metals: - metal = inorganictoken.args[0] - pydwarf.log.debug('Handling metal %s...' % metal) - itemtokens = inorganictoken.allprop(value_in=items) - if len(itemtokens) < len(items): - pydwarf.log.debug('Adding tokens to metal %s...' % metal) - # Remove existing item tokens from the list (To avoid making duplicates) - for itemtoken in itemtokens: - itemtoken.remove() - # Add new ones - templatetoken = inorganictoken.getlastprop('USE_MATERIAL_TEMPLATE') - addaftertoken = templatetoken if templatetoken else inorganictoken - for item in items: - addaftertoken.add(item) - modified += 1 - else: - pydwarf.log.debug('Metal %s already allows all the item types specified, skipping.' % metal) - - # All done - if modified > 0: - return pydwarf.success('Added tokens to %d metals.' % modified) + # Turn the item names into a list of tokens + itemtokens = [raws.token(value=item) for item in items] + + # Apply to each metal + affected = df.allobj(type='INORGANIC', id_in=metals).each( + lambda token: ( + token.removeallprop(value_in=items), # Remove existing tokens first to prevent duplicates when adding + token.addprop(raws.token.copy(itemtokens)) # And now add the specified tokens + ) + ) + + # All done! + if affected: + return pydwarf.success('Affected %d metals.' % len(affected)) else: - return pydwarf.failure('No tokens were added to any metals.') + return pydwarf.failure('Affected no metals.') diff --git a/scripts/pineapple/pydwarf.noaquifers.py b/scripts/pineapple/pydwarf.noaquifers.py index dc7fc6b..cec19c1 100644 --- a/scripts/pineapple/pydwarf.noaquifers.py +++ b/scripts/pineapple/pydwarf.noaquifers.py @@ -2,15 +2,16 @@ @pydwarf.urist( name = 'pineapple.noaquifers', - version = '1.0.0', + version = '1.0.1', author = 'Sophie Kirschner', description = 'Removes all AQUIFER tokens.', compatibility = (pydwarf.df_0_27, pydwarf.df_0_28, pydwarf.df_0_3x, pydwarf.df_0_40) ) def noaquifers(df): - aquifers = df.all('AQUIFER') + # Do the removing + aquifers = df.removeall('AQUIFER') + # All done! if len(aquifers): - for aquifer in aquifers: aquifer.remove() return pydwarf.success('Removed %d AQUIFER tokens.' % len(aquifers)) else: return pydwarf.failure('Found no AQUIFER tokens.') diff --git a/scripts/pineapple/pydwarf.noexotic.py b/scripts/pineapple/pydwarf.noexotic.py index 99e2d14..2f2747c 100644 --- a/scripts/pineapple/pydwarf.noexotic.py +++ b/scripts/pineapple/pydwarf.noexotic.py @@ -2,16 +2,17 @@ @pydwarf.urist( name = 'pineapple.noexotic', - version = '1.0.0', + version = '1.0.1', author = 'Sophie Kirschner', description = 'Replaces all [PET_EXOTIC] and [MOUNT_EXOTIC] tags with their non-exotic counterparts.', compatibility = (pydwarf.df_0_34, pydwarf.df_0_40) ) def noexotic(df): - pets = df.all('PET_EXOTIC') - mounts = df.all('MOUNT_EXOTIC') - for token in pets: token.value = 'PET' - for token in mounts: token.value = 'MOUNT' + # Do the removing + pets = df.all('PET_EXOTIC').each(lambda token: token.setvalue('PET')) + mounts = df.all('MOUNT_EXOTIC').each(lambda token: token.setvalue('MOUNT')) + + # All done! if len(pets) or len(mounts): return pydwarf.success('Replaced %d PET_EXOTIC and %d MOUNT_EXOTIC tokens.' % (len(pets), len(mounts))) else: diff --git a/scripts/pineapple/pydwarf.nograzers.py b/scripts/pineapple/pydwarf.nograzers.py index 52c2fa3..f21dfce 100644 --- a/scripts/pineapple/pydwarf.nograzers.py +++ b/scripts/pineapple/pydwarf.nograzers.py @@ -2,18 +2,18 @@ @pydwarf.urist( name = 'pineapple.nograzers', - version = '1.0.0', + version = '1.0.1', author = 'Sophie Kirschner', description = 'Removes all [GRAZER] and [STANDARD_GRAZER] tokens.', compatibility = (pydwarf.df_0_34, pydwarf.df_0_40) ) def nograzers(df): - grazers = df.all(exact_value='GRAZER') - standardgrazers = df.all('STANDARD_GRAZER') - for grazer in grazers: grazer.remove() - for grazer in standardgrazers: grazer.remove() + # Do the removing + grazers = df.removeall('GRAZER') + standardgrazers = df.removeall('STANDARD_GRAZER') + + # All done! if len(grazers) or len(standardgrazers): return pydwarf.success('Removed %d GRAZER and %d STANDARD_GRAZER tokens.' % (len(grazers), len(standardgrazers))) else: - return pydwarf.failure('I found no grazer tokens to replace.') - + return pydwarf.failure('I found no grazer tokens to remove.') diff --git a/scripts/pineapple/pydwarf.utils.py b/scripts/pineapple/pydwarf.utils.py index fd46678..07b837e 100644 --- a/scripts/pineapple/pydwarf.utils.py +++ b/scripts/pineapple/pydwarf.utils.py @@ -5,62 +5,29 @@ @pydwarf.urist( name = 'pineapple.utils.addtoentity', - version = '1.0.0', + version = '1.0.1', author = 'Sophie Kirschner', description = '''A simple utility script which adds tokens to entities.''', arguments = { 'entities': 'Adds tokens to these entities.', - '**kwargs': ''''Should map keywords to iterables. For example, setting the argument - permitted_job=('MINER', 'CARPENTER') would result in the tokens - [PERMITTED_JOB:MINER] and [PERMITTED_JOB:CARPENTER] being added to each of the - listed entities if they aren't present already.''' + 'tokens': 'A string or collection of tokens to add to each entity.', }, compatibility = '.*' ) -def addtoentity(df, entities, **kwargs): - pydwarf.log.debug('Adding tokens to %d entities.' % len(entities)) - added = 0 +def addtoentity(df, entities, tokens): + if isinstance(entities, basestring): entities = (entities,) + pydwarf.log.debug('Adding tokens to entities %s.' % ', '.join(str(ent) for ent in entities)) entitytokens = df.allobj(type='ENTITY', id_in=entities) + + for entitytoken in entitytokens: + entitytoken.addprop(tokens) + if isinstance(tokens, raws.queryable): tokens = raws.token.copy(tokens) # TODO: What about other iterables containing token objects, e.g. lists and tuples? + if len(entitytokens) != len(entities): - return pydwarf.failure() + return pydwarf.failure('Failed to add tokens to all given entities because only %d of %d exist.' % (len(entitytokens), len(entities))) else: - for permittype, permititems in kwargs.iteritems(): - permitvalue = permittype.upper() - pydwarf.log.debug('Handling tokens of type %s.' % permitvalue) - for permititem in permititems: - for entitytoken in entitytokens: - if not entitytoken.getprop(exact_value=permitvalue, exact_args=(permititem,)): - entitytoken.addprop(raws.token(value=permitvalue, args=[permititem])) - added += 1 - return pydwarf.success('Added %d permitted things to %d entities.' % (added, len(entitytokens))) - - + return pydwarf.success('Added tokens to %d entities.' % len(entitytokens)) -@pydwarf.urist( - name = 'pineapple.utils.addreaction', - version = '1.0.1', - author = 'Sophie Kirschner', - description = '''A simple utility script which adds a single reaction.''', - arguments = { - 'id': 'ID of the reaction to add.', - 'tokens': 'The tokens that should be added immediately after the REACTION:ID token.', - 'add_to_file': 'Adds the reaction to this file. Defaults to reaction_custom.', - 'permit_entities': '''An iterable containing entities to which to add - PERMITTED_REACTION tokens to. When set to None, no such tokens will be added.''' - }, - compatibility = '.*' -) -def addreaction(df, id, tokens, add_to_file='reaction_custom', permit_entities=None): - if permit_entities is not None and (not addtoentity(df, permit_entities, permitted_reaction=(id,)).success): - return pydwarf.failure('Failed to add permitted reactions to entites.') - else: - if df.getobj(type='REACTION', exact_id=id): - return pydwarf.failure('Reaction %s already exists.' % id) - else: - rfile = df.getfile(add_to_file, create='OBJECT:REACTION') - rfile.add(raws.token(value='REACTION', args=[id], prefix='\n\n')).add(tokens) - return pydwarf.success('Added reaction %s to file %s and entities %s.' % (id, add_to_file, permit_entities)) - @pydwarf.urist( @@ -105,3 +72,220 @@ def objecttokens(df, object_type, token, add_to=None, remove_from=None): return pydwarf.success('Added %d %s tokens and removed %d from object type %s.' % (added, token, removed, object_type)) else: return pydwarf.failure('Didn\'t add or remove any %s tokens.' % token) + + + +@pydwarf.urist( + name = 'pineapple.utils.addhack', + version = '1.0.0', + author = 'Sophie Kirschner', + description = '''Utility script for adding a new DFHack script.''', + arguments = { + 'auto_run': '''If set to True, a line will be added to dfhack.init containing only + the name of the added script. If set to None, no such line will be added. If set + to an arbitrary string, that string will be added as a new line at the end of + dfhack.init.''', + '**kwargs': '''Other named arguments will be passed on to the dir.add method used to + create the file object corresponding to the added script.''' + }, + compatibility = '.*' +) +def addhack(df, auto_run, **kwargs): + name = kwargs.get('name', kwargs.get('path', 'unnamed')) + + pydwarf.log.debug('Adding new file %s.' % name) + file = df.add(**kwargs) + + if auto_run: + if auto_run is True: auto_run = '\n%s' % file.name + pydwarf.log.debug('Appending line %s to the end of dfhack.init.' % auto_run) + + if 'dfhack.init' not in df: + if 'dfhack.init-example' in df: + pydwarf.log.info('Copying dfhack.init-example to new file dfhack.init before adding new content to the file.') + init = df['dfhack.init-example'].copy().bin() + init.name = 'dfhack.init' + df.add(file=init) + else: + return pydwarf.failure('Failed to locate dfhack.init or dfhack.init-example.') + else: + init = df['dfhack.init'].bin() + + init.add('\n%s # Added by PyDwarf\n' % auto_run) + return pydwarf.success('Added new file %s and appended line %s to dfhack.init.' % (name, auto_run)) + + else: + return pydwarf.success('Added new file %s.' % name) + + + +@pydwarf.urist( + name = 'pineapple.utils.addobject', + version = '1.0.0', + author = 'Sophie Kirschner', + description = '''Utility script for adding a new object to the raws.''', + arguments = { + 'add_to_file': '''The name of the file to add the object to. If it doesn't exist already + then the file is created anew. The string is formatted such that %(type)s is + replaced with the object_header, lower case.''', + 'tokens': '''The tokens belonging to the object to create.''', + 'type': '''Specifies the object type. If type and id are left unspecified, the first + token of the tokens argument is assumed to be the object's [TYPE:ID] token and the + type and id arguments are taken out of that.''', + 'id': '''Specifies the object id. If type and id are left unspecified, the first + token of the tokens argument is assumed to be the object's [TYPE:ID] token and the + type and id arguments are taken out of that.''', + 'permit_entities': '''For relevant object types such as reactions, buildings, and items, + if permit_entities is specified then tokens are added to those entities to permit + the added object.''', + 'item_rarity': '''Most items, when adding tokens to entities to permit them, accept an + optional second argument specifying rarity. It should be one of 'RARE', 'UNCOMMON', + 'COMMON', or 'FORCED'. This argument can be used to set that rarity.''', + 'object_header': '''When the object is added to a file which doesn't already exist, + an [OBJECT:TYPE] token must be added at its beginning. This argument, if specified, + provides the type in that token. Otherwise, when the argument is left set to None, + the type will be automatically decided.''' + }, + compatibility = '.*' +) +def addobject(df, add_to_file, tokens, type=None, id=None, permit_entities=None, item_rarity=None, object_header=None): + # If type and id weren't explicitly given then assume the first given token is the TYPE:ID header and get the info from there. + header_in_tokens = type is None and id is None + header = None + if header_in_tokens: + if isinstance(tokens, basestring): tokens = raws.token.parse(tokens) + header = tokens[0] + type = header.value + id = header.arg() + pydwarf.log.debug('Extracted object type %s and id %s from given tokens.' % (type, id)) + + # Get the applicable object dict which knows how to map TYPE:ID to its corresponding OBJECT:TYPE header. + if object_header is None: + object_header = raws.objects.headerforobject(type) + + # If add_to_file already exists, fetch it. Otherwise add it to the raws. + add_to_file = add_to_file % {'type': object_header.lower()} + if add_to_file in df: + file = df.getfile(add_to_file) + else: + file = df.add(add_to_file) + file.add(raws.token(value='OBJECT', args=[object_header])) + pydwarf.log.debug('Added new file %s to dir.' % add_to_file) + + # Add the object itself to the raws. + if not header_in_tokens: header = file.add(raws.token(value=type, args=[id])) + file.add(tokens) + + # Add tokens to entities to permit the use of this object. + if permit_entities: + response = permitobject( + df, + type = type, + id = id, + permit_entities = permit_entities, + item_rarity = item_rarity + ) + if not response: return response + + # All done! + return pydwarf.success('Added object %s to file %s.' % (header, file)) + + + +@pydwarf.urist( + name = 'pineapple.utils.addobjects', + version = '1.0.0', + author = 'Sophie Kirschner', + description = '''Utility script for adding several new objects to the raws at once.''', + arguments = { + 'add_to_file': '''The name of the file to add the object to. If it doesn't exist already + then the file is created anew. The string is formatted such that %(type)s is + replaced with the object_header, lower case.''', + 'objects': '''An iterable containing tokens belonging to the objects to add.''', + '**kwargs': 'Passed on to pineapple.utils.addobject.', + }, + compatibility = '.*' +) +def addobjects(df, add_to_file, objects, **kwargs): + for obj in objects: + response = addobject(df, add_to_file, **kwargs) + if not response.success: return response + return response.success('Added %d objects.' % len(objects)) + + + +@pydwarf.urist( + name = 'pineapple.utils.permitobject', + version = '1.0.0', + author = 'Sophie Kirschner', + description = '''Utility script for permitting an object with entities.''', + arguments = { + 'type': '''Specifies the object type.''', + 'id': '''Specifies the object id.''', + 'permit_entities': '''For relevant object types such as reactions, buildings, and items, + if permit_entities is specified then tokens are added to those entities to permit + the added object.''', + 'item_rarity': '''Some items, when adding tokens to entities to permit them, accept an + optional second argument specifying rarity. It should be one of 'RARE', 'UNCOMMON', + 'COMMON', or 'FORCED'. This argument can be used to set that rarity.''' + }, + compatibility = '.*' +) +def permitobject(df, type=None, id=None, permit_entities=None, item_rarity=None): + # Decide what tokens need to be added to the entities based on the object type + if type == 'REACTION': + tokens = raws.token(value='PERMITTED_REACTION', args=[id]) + elif type.startswith('BUILDING_'): + tokens = raws.token(value='PERMITTED_BUILDING', args=[id]) + elif type.startswith('ITEM_'): + value = type.split('_')[1] + args = [id, item_rarity] if item_rarity else [id] + tokens = raws.token(value=value, args=args) + else: + tokens = None + + pydwarf.log.debug('Permitting object [%s:%s] for %d entities.' % (type, id, len(permit_entities))) + + # Actually add those tokens + if tokens is None: + return pydwarf.success('Didn\'t actually permit object [%s:%s] because objects of this type cannot be permitted.' % (type, id)) + elif not permit_entities: + return pydwarf.failure('No entities were given for permitting.') + else: + response = addtoentity( + df, + entities = permit_entities, + tokens = tokens + ) + if not response: + return response + else: + return pydwarf.success('Permitted object [%s:%s] for %d entities.' % (type, id, len(permit_entities))) + + + +@pydwarf.urist( + name = 'pineapple.utils.permitobjects', + version = '1.0.0', + author = 'Sophie Kirschner', + description = '''Utility script for permitting several objects at once with entities.''', + arguments = { + 'objects': '''An iterable containing either tokens or type, id tuples representing objects + to be permitted.''', + '**kwargs': 'Passed on to pineapple.utils.permitobject.', + }, + compatibility = '.*' +) +def permitobjects(df, objects, **kwargs): + pydwarf.log.debug('Permitting %d objects.' % len(objects)) + for item in objects: + if isinstance(item, raws.token): + type, id = item.value, item.arg() + elif isinstance(item, basestring): + type, id = item.split(':') + else: + type, id = item + response = permitobject(df, type, id, **kwargs) + if not response: return response + return pydwarf.success('Permitted %d objects.' % len(objects)) + \ No newline at end of file diff --git a/scripts/pineapple/pydwarf.woodmechanisms.py b/scripts/pineapple/pydwarf.woodmechanisms.py index 7a33cfb..95a9784 100644 --- a/scripts/pineapple/pydwarf.woodmechanisms.py +++ b/scripts/pineapple/pydwarf.woodmechanisms.py @@ -2,25 +2,17 @@ -wood_mechanisms_reaction = ''' - [NAME:craft wooden mechanisms] - [BUILDING:CRAFTSMAN:NONE] - [REAGENT:A:%d:WOOD:NONE:NONE:NONE] - [PRODUCT:100:1:TRAPPARTS:NONE:GET_MATERIAL_FROM_REAGENT:A:NONE] - [SKILL:MECHANICS] -''' - default_log_count = 1 default_entities = ['MOUNTAIN', 'PLAINS'] -default_file = 'reaction_woodmechanisms_pineapple' +default_file = 'raw/objects/reaction_woodmechanisms_pineapple.txt' @pydwarf.urist( name = 'pineapple.woodmechanisms', - version = '1.0.0', + version = '1.0.1', author = 'Sophie Kirschner', description = '''Allows construction of wooden mechanisms at the craftdwarf's workshop. Inspired by/stolen from Rubble's Wooden Mechanisms mod.''', @@ -32,10 +24,17 @@ compatibility = (pydwarf.df_0_2x, pydwarf.df_0_3x, pydwarf.df_0_40) ) def woodmechanisms(df, log_count=default_log_count, entities=default_entities, add_to_file=default_file): - return pydwarf.urist.getfn('pineapple.utils.addreaction')( + return pydwarf.urist.getfn('pineapple.utils.addobject')( df, + type = 'REACTION', id = 'WOODEN_MECHANISMS_PINEAPPLE', - tokens = wood_mechanisms_reaction % log_count, + tokens = ''' + [NAME:craft wooden mechanisms] + [BUILDING:CRAFTSMAN:NONE] + [REAGENT:A:%d:WOOD:NONE:NONE:NONE] + [PRODUCT:100:1:TRAPPARTS:NONE:GET_MATERIAL_FROM_REAGENT:A:NONE] + [SKILL:MECHANICS] + ''' % log_count, add_to_file = add_to_file, permit_entities = entities ) diff --git a/scripts/pineapple/raws/greensteel/inorganic_green_steel_pineapple.txt b/scripts/pineapple/raw/greensteel/inorganic_green_steel_pineapple.txt similarity index 100% rename from scripts/pineapple/raws/greensteel/inorganic_green_steel_pineapple.txt rename to scripts/pineapple/raw/greensteel/inorganic_green_steel_pineapple.txt diff --git a/scripts/pineapple/raws/greensteel/reaction_green_steel_pineapple.txt b/scripts/pineapple/raw/greensteel/reaction_green_steel_pineapple.txt similarity index 100% rename from scripts/pineapple/raws/greensteel/reaction_green_steel_pineapple.txt rename to scripts/pineapple/raw/greensteel/reaction_green_steel_pineapple.txt diff --git a/scripts/pineapple/readme.md b/scripts/pineapple/readme.md index 8ffce6e..fbcc6fd 100644 --- a/scripts/pineapple/readme.md +++ b/scripts/pineapple/readme.md @@ -38,6 +38,21 @@ Can be used to merge and apply changes made to modified raws. Attempting to appl Adds discipline as a natural skill to creatures that shouldn't be running away so easily. Creatures given bonuses include civilized creatures, trainable creatures, evil creatures, megabeasts, and more. +## easypatch + +Add a file or a bunch of tokens and the script handles entity permissions all on its own. Good for adding small mods, for example: + +``` json +{ + "name": "pineapple.easypatch", + "args": { + "files": "path/to/raws", + "loc": "raw/objects", + "permit_entities": "MOUNTAIN" + } +} +``` + ## flybears A simple example script which adds a `[FLIER]` tag to all female vanilla bears. @@ -94,15 +109,31 @@ With default settings it becomes possible to craft items using the normally unco ### addtoentity -A utility script intended for adding things like `[PERMITTED_REACTION]` to entities. +Utility script intended for adding things like `[PERMITTED_REACTION:ID]` to entities. -### addreaction +### objecttokens -A utility script which adds a reaction as well as `[PERMITTED_REACTION]` tokens to entities. +Utility script which abstracts adding tokens to or removing them from objects, either all at once or given an iterable containing object IDs. -### objecttokens +### addhack + +Utility script for adding a DFHack script. It can also add a line to dfhack.init to automatically run the script when the game starts. + +### addobject + +Utility script adds an object and handles permitting reactions and buildings and such with entities. + +### addobject + +Utility script adds multiple objects using pineapple.utils.addobject. + +### permitobject + +Utility script adds tokens like `[PERMITTED_REACTION:ID]` to entities given an object. + +### permitobjects -A utility script which abstracts adding tokens to or removing them from objects, either all at once or given an iterable containing object IDs. +Utility script permits multiple objects using pineapple.utils.permitobject. ## woodmechanisms diff --git a/scripts/pkdawson/pydwarf.vegan.py b/scripts/pkdawson/pydwarf.vegan.py new file mode 100644 index 0000000..1b2b3ed --- /dev/null +++ b/scripts/pkdawson/pydwarf.vegan.py @@ -0,0 +1,108 @@ +import pydwarf + + + +vegan_reactions = { + 'CLOTH_QUIVER_PKDAWSON': ''' + [NAME:weave cloth quiver] + [BUILDING:CRAFTSMAN:NONE] + [REAGENT:mat:1:CLOTH:NONE:NONE:NONE][DOES_NOT_DETERMINE_PRODUCT_AMOUNT] + [PRODUCT:100:1:QUIVER:NONE:GET_MATERIAL_FROM_REAGENT:mat:NONE] + [SKILL:CLOTHESMAKING] + ''', + 'CLOTH_BACKPACK_PKDAWSON': ''' + [NAME:weave cloth backpack] + [BUILDING:CRAFTSMAN:NONE] + [REAGENT:mat:1:CLOTH:NONE:NONE:NONE][DOES_NOT_DETERMINE_PRODUCT_AMOUNT] + [PRODUCT:100:1:BACKPACK:NONE:GET_MATERIAL_FROM_REAGENT:mat:NONE] + [SKILL:CLOTHESMAKING] + ''', +} + +default_entities = ['MOUNTAIN', 'PLAINS'] + +default_file = 'raw/objects/reaction_vegan_pkdawson.txt' + +default_lua_file = 'hack/scripts/autolabor-vegan-pkdawson.lua' + +lua_content = ''' +default_labors = { + %(labors)s +} +for i, labor in ipairs(nonvegan_labors) do + dfhack.run_command(string.format("autolabor %(format)s 0 0", labor)) +end +''' + +default_labors = [ + 'BUTCHER', + 'TRAPPER', + 'DISSECT_VERMIN', + 'LEATHER', + 'TANNER', + 'MAKE_CHEESE', + 'MILK', + 'FISH', + 'CLEAN_FISH', + 'DISSECT_FISH', + 'HUNT', + 'BONE_CARVE', + 'SHEARER', + 'BEEKEEPING', + 'WAX_WORKING', + 'GELD', +] + +def format_lua_content(content, labors): + return content % {'labors': '\n '.join('"%s"' % labor for labor in labors), 'format': '%s'} + + + +@pydwarf.urist( + name = 'pkdawson.vegan', + version = '1.0.0', + author = ('Patrick Dawson', 'Sophie Kirschner'), + description = '''Adds reactions to the craftdwarf's workshop for making quivers and + backpacks from cloth, which normally require leather. Also adds a DFHack script + which disables non-vegan labors using autolabor.''', + arguments = { + 'labors': '''These labors will be disabled using a DFHack script. If set to None + then no DFHack script will be written. The default labors are %s. + ''' % ', '.join(default_labors), + 'lua_file': '''The DFHack script will be added to this path, relative to DF's + root directory. If set to None then no DFHack script will be written.''', + 'auto_run': '''If set to True, and if the DFHack script is added, then a line + will be added to the end of dfhack.init which runs this script on startup. + If set to False then the script will wait to be run manually.''', + 'entities': 'Adds the reaction to these entities. Defaults to MOUNTAIN and PLAINS.', + 'add_to_file': 'Adds the reaction to this file.' + }, + compatibility = pydwarf.df_0_40 +) +def vegan(df, labors=default_labors, lua_file=default_lua_file, auto_run=False, entities=default_entities, add_to_file=default_file): + # Add the reactions + addreaction = pydwarf.urist.getfn('pineapple.utils.addobject') + for reactionid, reactioncontent in vegan_reactions.iteritems(): + pydwarf.log.debug('Adding reaction %s.' % reactionid) + response = addreaction( + df, + type = 'REACTION', + id = reactionid, + tokens = reactioncontent, + add_to_file = add_to_file, + permit_entities = entities + ) + if not response.success: return response + + # Add the dfhack script + if labors and lua_file: + pydwarf.log.debug('Adding DFHack script %s.' % lua_file) + pydwarf.urist.getfn('pineapple.utils.addhack')( + df, + name = lua_file, + content = format_lua_content(lua_content, labors), + auto_run = auto_run + ) + + # All done + return pydwarf.success() diff --git a/scripts/pkdawson/readme.md b/scripts/pkdawson/readme.md new file mode 100644 index 0000000..b00d989 --- /dev/null +++ b/scripts/pkdawson/readme.md @@ -0,0 +1,30 @@ +df-vegan +======== + +Created by Patrick Dawson. (https://github.com/pkdawson) +Rewritten for PyDwarf by Sophie Kirschner. (http://www.pineapplemachine.com) + +A few tweaks are required to make vegan dwarves completely practical in Dwarf Fortress. This mod adds cloth quivers and backpacks, which can be made at a Craftsdwarf's Workshop, using the Clothesmaking skill. + +I'd also recommend turning off artifacts (strange moods), which often demand bone or other animal products. + +To do this challenge properly, you should never use any kind of animal product. That includes no wool, no silk, and no beekeeping. Keeping pets is fine, but training or using war dogs (or other war animals) would be unethical. + +Here's a list of labors that are disabled by default, using a DFHack script: + +- Hunting +- Trapping +- Small Animal Dissection +- Butchery +- Tanning +- Cheese Making +- Milking +- Shearing +- Beekeeping +- Gelding +- Fishing +- Fish Cleaning +- Fish Dissection +- Leatherworking +- Bone Carving +- Wax Working diff --git a/scripts/putnam/pydwarf.materialsplus.py b/scripts/putnam/pydwarf.materialsplus.py index b7ada0b..e2de445 100644 --- a/scripts/putnam/pydwarf.materialsplus.py +++ b/scripts/putnam/pydwarf.materialsplus.py @@ -2,91 +2,98 @@ import pydwarf import raws -# Utility function for putting new properties after an inorganic's USE_MATERIAL_TEMPLATE token, if it has one -# Otherwise, the property is just added after the INORGANIC object token. -def addaftertemplate(inorganic, addition): - template = inorganic.getuntil(exact_value='USE_MATERIAL_TEMPLATE', until_exact_value='INORGANIC') - addafter = template if template else inorganic - return addafter.add(addition) + + +mats_dir = pydwarf.rel(__file__, 'raw/materialsplus') + +default_entities = 'MOUNTAIN' + +add_paths = [os.path.join(mats_dir, path) for path in [ + 'inorganic_alloys_mat_plus.txt', + 'inorganic_metals_mat_plus.txt', + 'inorganic_other_mat_plus.txt', + 'item_mat_plus.txt' +]] + +patch_paths = [os.path.join(mats_dir, path) for path in [ + 'reaction_alloys_mat_plus.txt', + 'reaction_production_mat_plus.txt' +]] + + + +add_properties = [ + ( + # Identifier for making the log easier to understand + 'zircon', + # Regex to match inorganic IDs + '.* ZIRCON', + # Add these properties + 'MATERIAL_REACTION_PRODUCT:KROLL_PROCESS:INORGANIC:ZIRCONIUM_PUTNAM' + ), + ( + 'beryl', + '.* BERYL|HELIODOR|MORGANITE|GOSHENITE|EMERALD', + 'REACTION_CLASS:BERYLLIUM' + ), + ( + 'silicon', + 'ANDESITE|OLIVINE|HORNBLENDE|SERPENTINE|ORTHOCLASE|MICROCLINE|MICA', + 'REACTION_CLASS:SILICON' + ), + ( + 'dolomite', + 'DOLOMITE', + 'REACTION_CLASS:PIDGEON_PROCESS' + ), + ( + 'cromite', + 'CHROMITE', + '[METAL_ORE:CHROMIUM_PUTNAM:100][METAL_ORE:IRON:50]' + ), + ( + 'pyrolusite', + 'PYROLUSITE', + 'METAL_ORE:MANGANESE_PUTNAM:100' + ), +] + + @pydwarf.urist( name = 'putnam.materialsplus', - version = '1.0.0', + version = '1.0.1', author = ('Putnam', 'Sophie Kirschner'), description = 'Adds a bunch of materials to the game.', compatibility = (pydwarf.df_0_34, pydwarf.df_0_40) ) -def materialsplus(dfraws): - exceptions = 0 - addedreactions = [] +def materialsplus(df, entities=default_entities): + # Add properties to various inorganics as defined by the add_properties dict + errors = 0 + for identifier, re_id, addprops in add_properties: + additions = df.allobj(type='INORGANIC', re_id=re_id).each( + lambda token: token.addprop(addprops) + ) + if len(additions): + pydwarf.log.debug('Added %s properties to %d inorganics.' % (identifier, len(additions))) + else: + errors += 1 + pydwarf.log.error('Failed to add %s properties because no matching inorganics were found.' % identifier) - try: - for zircon in dfraws.all(exact_value='INORGANIC', re_args=['.* ZIRCON']): - addaftertemplate(zircon, 'MATERIAL_REACTION_PRODUCT:KROLL_PROCESS:INORGANIC:ZIRCONIUM_PUTNAM') - pydwarf.log.debug('Added reaction to zircons.') - except: - pydwarf.log.exception('Failed to add reaction to zircons.') - exceptions += 1 - - try: - for beryl in dfraws.all(exact_value='INORGANIC', re_args=['.* BERYL|HELIODOR|MORGANITE|GOSHENITE|EMERALD']): - addaftertemplate(beryl, 'REACTION_CLASS:BERYLLIUM') - pydwarf.log.debug('Added reaction to beryls.') - except: - pydwarf.log.exception('Failed to add reaction to beryls.') - exceptions += 1 + for path in add_paths: + pydwarf.log.debug('Adding file at %s.' % path) + df.add(path=path, loc='raw/objects') - try: - chromite = dfraws.get('INORGANIC:CHROMITE') - pyrolusite = dfraws.get('INORGANIC:PYROLUSITE') - addaftertemplate(chromite, '[METAL_ORE:CHROMIUM_PUTNAM:100][METAL_ORE:IRON:50]') - addaftertemplate(pyrolusite, 'METAL_ORE:MANGANESE_PUTNAM:100') - pydwarf.log.debug('Added titanium ores.') - except: - pydwarf.log.exception('Failed to add titanium ores.') - exceptions += 1 + for path in patch_paths: + response = pydwarf.urist.getfn('pineapple.easypatch')( + df, + files = path, + loc = 'raw/objects', + permit_entities = entities + ) + if not response: return response - try: - for silicon in dfraws.all(exact_value='INORGANIC', re_args=['ANDESITE|OLIVINE|HORNBLENDE|SERPENTINE|ORTHOCLASE|MICROCLINE|MICA']): - addaftertemplate(silicon, 'REACTION_CLASS:SILICON') - pydwarf.log.debug('Added silicon reactions.') - except: - pydwarf.log.exception('Failed to add silicon reactions.') - exceptions += 1 - - try: - dolomite = dfraws.get('INORGANIC:DOLOMITE') - addaftertemplate(dolomite, 'REACTION_CLASS:PIDGEON_PROCESS') - pydwarf.log.debug('Added reaction to dolomite.') - except: - pydwarf.log.exception('Failed to add reaction to dolomite.') - exceptions += 1 - - for root, dirs, files in os.walk(os.path.join(os.path.dirname(os.path.abspath(__file__)), 'Materials Plus')): - for filename in files: - suffix = '_mat_plus.txt' - if filename.endswith(suffix): - path = os.path.join(root, filename) - destname = 'putnam_%s' % filename[:-len(suffix)] - rfile = dfraws.getfile(destname) - if rfile: - pydwarf.log.debug('Appending data to file %s from %s...' % (destname, path)) - with open(path, 'rb') as matplusfile: rfile.add(pretty=matplusfile) - else: - with open(path, 'rb') as matplusfile: rfile = dfraws.addfile(rfile=raws.file(header=destname, rfile=matplusfile)) - pydwarf.log.debug('Adding data to new file %s.' % destname) - addedreactions += rfile.all(exact_value='REACTION', args_count=1) - - try: - mountain = dfraws.get('ENTITY:MOUNTAIN') - for reaction in addedreactions: - mountain.add(raws.token(value='PERMITTED_REACTION', args=[reaction.args[0]])) - pydwarf.log.debug('Added %d permitted reactions.' % len(addedreactions)) - except: - pydwarf.log.exception('Failed to add permitted reactions.') - exceptions += 1 - - if exceptions == 0: + if not errors: return pydwarf.success() else: - return pydwarf.failure('Failed to complete %d operations.' % exceptions) + return pydwarf.failure('Failed to add inorganic properties for %d groups.' % errors) diff --git a/scripts/putnam/pydwarf.microreduce.py b/scripts/putnam/pydwarf.microreduce.py index accc2b3..aef4b3f 100644 --- a/scripts/putnam/pydwarf.microreduce.py +++ b/scripts/putnam/pydwarf.microreduce.py @@ -1,26 +1,24 @@ -import os import pydwarf -import raws + + + +reduce_dir = pydwarf.rel(__file__, 'raw/microreduce') + +default_entities = 'MOUNTAIN' + + @pydwarf.urist( name = 'putnam.microreduce', - version = '1.0.0', + version = '1.0.1', author = ('Putnam', 'Sophie Kirschner'), description = 'A mod to reduce the amount of micromanagement in Dwarf Fortress. One-step soap making and clothesmaking!', compatibility = pydwarf.df_0_40 ) -def microreduce(dfraws): - mountain = dfraws.get('ENTITY:MOUNTAIN') - if mountain: - # Add files - genericpath = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'Microreduce', '%s.txt') - for filename in ('building_macro_fantastic', 'item_macro_fantastic', 'reaction_macro_fantastic'): - rfile = dfraws.addfile(path=genericpath % filename) - # Add PERMITTED_BUILDING and PERMITTED_REACTION tokens to ENTITY:MOUNTAIN - for building in rfile.all(re_value='BUILDING.*', args_count=1): - mountain.add(raws.token(value='PERMITTED_BUILDING', args=[building.args[0]])) - for reaction in rfile.all(exact_value='REACTION', args_count=1): - mountain.add(raws.token(value='REACTION', args=[reaction.args[0]])) - return pydwarf.success() - else: - return pydwarf.failure('Couldn\'t find ENTITY:MOUNTAIN.') +def microreduce(df, entities=default_entities): + return pydwarf.urist.getfn('pineapple.easypatch')( + df, + files = reduce_dir, + loc = 'raw/objects', + permit_entities = entities + ) diff --git a/scripts/putnam/Materials Plus/entity_default.txt b/scripts/putnam/raw/materialsplus/entity_default.txt similarity index 100% rename from scripts/putnam/Materials Plus/entity_default.txt rename to scripts/putnam/raw/materialsplus/entity_default.txt diff --git a/scripts/putnam/Materials Plus/inorganic_alloys_mat_plus.txt b/scripts/putnam/raw/materialsplus/inorganic_alloys_mat_plus.txt similarity index 100% rename from scripts/putnam/Materials Plus/inorganic_alloys_mat_plus.txt rename to scripts/putnam/raw/materialsplus/inorganic_alloys_mat_plus.txt diff --git a/scripts/putnam/Materials Plus/inorganic_metals_mat_plus.txt b/scripts/putnam/raw/materialsplus/inorganic_metals_mat_plus.txt similarity index 100% rename from scripts/putnam/Materials Plus/inorganic_metals_mat_plus.txt rename to scripts/putnam/raw/materialsplus/inorganic_metals_mat_plus.txt diff --git a/scripts/putnam/Materials Plus/inorganic_other_mat_plus.txt b/scripts/putnam/raw/materialsplus/inorganic_other_mat_plus.txt similarity index 100% rename from scripts/putnam/Materials Plus/inorganic_other_mat_plus.txt rename to scripts/putnam/raw/materialsplus/inorganic_other_mat_plus.txt diff --git a/scripts/putnam/Materials Plus/inorganic_stone_gem.txt b/scripts/putnam/raw/materialsplus/inorganic_stone_gem.txt similarity index 100% rename from scripts/putnam/Materials Plus/inorganic_stone_gem.txt rename to scripts/putnam/raw/materialsplus/inorganic_stone_gem.txt diff --git a/scripts/putnam/Materials Plus/inorganic_stone_layer.txt b/scripts/putnam/raw/materialsplus/inorganic_stone_layer.txt similarity index 100% rename from scripts/putnam/Materials Plus/inorganic_stone_layer.txt rename to scripts/putnam/raw/materialsplus/inorganic_stone_layer.txt diff --git a/scripts/putnam/Materials Plus/inorganic_stone_mineral.txt b/scripts/putnam/raw/materialsplus/inorganic_stone_mineral.txt similarity index 100% rename from scripts/putnam/Materials Plus/inorganic_stone_mineral.txt rename to scripts/putnam/raw/materialsplus/inorganic_stone_mineral.txt diff --git a/scripts/putnam/Materials Plus/inorganic_stone_minerals_mat_plus.txt b/scripts/putnam/raw/materialsplus/inorganic_stone_minerals_mat_plus.txt similarity index 100% rename from scripts/putnam/Materials Plus/inorganic_stone_minerals_mat_plus.txt rename to scripts/putnam/raw/materialsplus/inorganic_stone_minerals_mat_plus.txt diff --git a/scripts/putnam/Materials Plus/item_mat_plus.txt b/scripts/putnam/raw/materialsplus/item_mat_plus.txt similarity index 100% rename from scripts/putnam/Materials Plus/item_mat_plus.txt rename to scripts/putnam/raw/materialsplus/item_mat_plus.txt diff --git a/scripts/putnam/Materials Plus/reaction_alloys_mat_plus.txt b/scripts/putnam/raw/materialsplus/reaction_alloys_mat_plus.txt similarity index 100% rename from scripts/putnam/Materials Plus/reaction_alloys_mat_plus.txt rename to scripts/putnam/raw/materialsplus/reaction_alloys_mat_plus.txt diff --git a/scripts/putnam/Materials Plus/reaction_production_mat_plus.txt b/scripts/putnam/raw/materialsplus/reaction_production_mat_plus.txt similarity index 100% rename from scripts/putnam/Materials Plus/reaction_production_mat_plus.txt rename to scripts/putnam/raw/materialsplus/reaction_production_mat_plus.txt diff --git a/scripts/putnam/Microreduce/building_macro_fantastic.txt b/scripts/putnam/raw/microreduce/building_macro_fantastic.txt similarity index 100% rename from scripts/putnam/Microreduce/building_macro_fantastic.txt rename to scripts/putnam/raw/microreduce/building_macro_fantastic.txt diff --git a/scripts/putnam/Microreduce/entity_default.txt b/scripts/putnam/raw/microreduce/entity_default.txt similarity index 100% rename from scripts/putnam/Microreduce/entity_default.txt rename to scripts/putnam/raw/microreduce/entity_default.txt diff --git a/scripts/putnam/Microreduce/item_macro_fantastic.txt b/scripts/putnam/raw/microreduce/item_macro_fantastic.txt similarity index 100% rename from scripts/putnam/Microreduce/item_macro_fantastic.txt rename to scripts/putnam/raw/microreduce/item_macro_fantastic.txt diff --git a/scripts/putnam/Microreduce/reaction_macro_fantastic.txt b/scripts/putnam/raw/microreduce/reaction_macro_fantastic.txt similarity index 100% rename from scripts/putnam/Microreduce/reaction_macro_fantastic.txt rename to scripts/putnam/raw/microreduce/reaction_macro_fantastic.txt diff --git a/scripts/putnam/readme.md b/scripts/putnam/readme.md new file mode 100644 index 0000000..4ad4cff --- /dev/null +++ b/scripts/putnam/readme.md @@ -0,0 +1,36 @@ +# Fantastic Mini-Mods + +Created by Putnam. (http://putnam3145.github.io/) +Rewritten for PyDwarf by Sophie Kirschner. (http://www.pineapplemachine.com) + +## Materials Plus + +Adds a bunch of materials to the game. + +New reactions in materials plus are: + +1. Steel making using manganese. 3 bars of iron and 1 bar of manganese will get you 4 bars of steel; one iron ore and one manganese ore will get you 4 steel, 3 manganese and 1 iron. +2. High-speed steel making. A slightly stronger variation of steel. Takes 6 bars of steel and one bar each of vanadium, chromium, tungsten and molybdenum and gets you 10 bars of high-speed steel. +3. Beryllium refining. Makes a pair of tokens that automatically goes through the process as available. The process requires: + 1. one [beryllium-bearing boulder OR 4 beryllium-bearing gems](https://github.com/Putnam3145/Fantastic-Mini-Mods/wiki/Materials-plus-Reaction-info) + 2. two fluorite gems + 3. a bucket of sulfuric acid, which itself requires + 1. a boulder of brimstone + 2. any item that contains vanadium (will be preserved) + 3. a bucket + 4. and a boulder of rock salt. This will get you 4 bars of beryllium. Beryllium is very light and stronger than iron, so it should be worth it. +4. The Kroll Process to refine zirconium or titanium. Makes a pair of tokens that automatically goes through the process as available. The process requires: + 1. one [titanium/zirconium-bearing boulder OR 4 titanium/zirconium-bearing gems](https://github.com/Putnam3145/Fantastic-Mini-Mods/wiki/Materials-plus-Reaction-info) + 2. a bar of coke + 3. two bars of a reducer metal (magnesium only for now), which itself requires: + 1. A [magnesium-bearing boulder](https://github.com/Putnam3145/Fantastic-Mini-Mods/wiki/Materials-plus-Reaction-info) + 2. A [silicon-bearing boulder](https://github.com/Putnam3145/Fantastic-Mini-Mods/wiki/Materials-plus-Reaction-info) + 4. This will all get you 4 bars of zirconium/titanium and give you back one bar of the reducer metal. + +## Microreduce + +Reactions included to reduce micromanagement in microreduce are: + +* Steel making from ore; take 4 bars of coal and a boulder of iron ore to make 4 bars of steel. +* Soap macro; automatically makes the lye required to make soap and makes soap as soon as materials are available. +* Full set of clothes; makes a shirt, pants and pair of shoes from 4 cloth items, without needing any micromanagement to make all that individually. diff --git a/scripts/putnam/readme.txt b/scripts/putnam/readme.txt deleted file mode 100644 index 0ed8460..0000000 --- a/scripts/putnam/readme.txt +++ /dev/null @@ -1,2 +0,0 @@ -Fantastic Mini-Mods created by Putnam. (http://putnam3145.github.io/) -Materials Plus and Microreduce ported to PyDwarf by Sophie Kirschner. (http://www.pineapplemachine.com) diff --git a/scripts/shukaro/pydwarf.creationforge.py b/scripts/shukaro/pydwarf.creationforge.py index 6a34071..c99f547 100644 --- a/scripts/shukaro/pydwarf.creationforge.py +++ b/scripts/shukaro/pydwarf.creationforge.py @@ -1,16 +1,16 @@ -import sys -import os +import pydwarf -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -import pydwarf -import shukaroutils -forgedir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'creationforge') +creation_dir = pydwarf.rel(__file__, 'raw/creationforge') + +default_entities = ('MOUNTAIN',) + + @pydwarf.urist( name = 'shukaro.creationforge', - version = '1.0.0', + version = '1.0.1', author = ('Shukaro', 'Sophie Kirschner'), description = '''This is a simple workshop I modded in to help test custom reactions, buildings, and creatures. It's used to create various different items so that you @@ -18,5 +18,10 @@ useful tool to people, even if it's just to look at the raw formatting. ''', compatibility = (pydwarf.df_0_40, pydwarf.df_0_3x) ) -def higherlearning(dfraws, entities=shukaroutils.default_entities): - return shukaroutils.addraws(pydwarf, dfraws, forgedir, entities) +def creationforge(df, entities=default_entities): + return pydwarf.urist.getfn('pineapple.easypatch')( + df, + files = creation_dir, + loc = 'raw/objects', + permit_entities = entities + ) diff --git a/scripts/shukaro/pydwarf.higherlearning.py b/scripts/shukaro/pydwarf.higherlearning.py index d8227b0..08f5388 100644 --- a/scripts/shukaro/pydwarf.higherlearning.py +++ b/scripts/shukaro/pydwarf.higherlearning.py @@ -1,12 +1,10 @@ -import sys -import os +import pydwarf -sys.path.append(os.path.dirname(os.path.abspath(__file__))) -import pydwarf -import shukaroutils -librarydir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'higherlearning') +library_dir = pydwarf.rel(__file__, 'raw/higherlearning') + +default_entities = 'MOUNTAIN' librarian_position = ''' [POSITION:LIBRARIAN] @@ -21,11 +19,14 @@ [COLOR:5:0:0] [DUTY_BOUND] [REQUIRED_BEDROOM:1] - [REQUIRED_OFFICE:1]''' + [REQUIRED_OFFICE:1] +''' + + @pydwarf.urist( name = 'shukaro.higherlearning', - version = '1.0.0', + version = '1.0.1', author = ('Shukaro', 'Sophie Kirschner'), description = '''Have you ever wondered to yourself, "Man, my dwarves are such idiots, I wish I could chisel some intelligence into their heads."? No? Then, er, disregard @@ -49,5 +50,20 @@ }, compatibility = (pydwarf.df_0_40, pydwarf.df_0_3x) # I think it should be compatible with 0.40.x? Haven't actually tried it yet. ) -def higherlearning(dfraws, entities=shukaroutils.default_entities): - return shukaroutils.addraws(pydwarf, dfraws, librarydir, entities, librarian_position) +def higherlearning(df, entities=default_entities): + # Add librarian position + response = pydwarf.urist.getfn('pineapple.utils.addtoentity')( + df, + entities = entities, + tokens = librarian_position + ) + if not response: return response + + # Add buildings, reactions, items from raws + return pydwarf.urist.getfn('pineapple.easypatch')( + df, + files = library_dir, + loc = 'raw/objects', + permit_entities = entities + ) + \ No newline at end of file diff --git a/scripts/shukaro/creationforge/building_forge_shukaro.txt b/scripts/shukaro/raw/creationforge/building_forge_shukaro.txt similarity index 100% rename from scripts/shukaro/creationforge/building_forge_shukaro.txt rename to scripts/shukaro/raw/creationforge/building_forge_shukaro.txt diff --git a/scripts/shukaro/creationforge/reaction_forge_shukaro.txt b/scripts/shukaro/raw/creationforge/reaction_forge_shukaro.txt similarity index 100% rename from scripts/shukaro/creationforge/reaction_forge_shukaro.txt rename to scripts/shukaro/raw/creationforge/reaction_forge_shukaro.txt diff --git a/scripts/shukaro/higherlearning/building_library_shukaro.txt b/scripts/shukaro/raw/higherlearning/building_library_shukaro.txt similarity index 100% rename from scripts/shukaro/higherlearning/building_library_shukaro.txt rename to scripts/shukaro/raw/higherlearning/building_library_shukaro.txt diff --git a/scripts/shukaro/higherlearning/item_library_shukaro.txt b/scripts/shukaro/raw/higherlearning/item_library_shukaro.txt similarity index 100% rename from scripts/shukaro/higherlearning/item_library_shukaro.txt rename to scripts/shukaro/raw/higherlearning/item_library_shukaro.txt diff --git a/scripts/shukaro/higherlearning/reaction_library_shukaro.txt b/scripts/shukaro/raw/higherlearning/reaction_library_shukaro.txt similarity index 100% rename from scripts/shukaro/higherlearning/reaction_library_shukaro.txt rename to scripts/shukaro/raw/higherlearning/reaction_library_shukaro.txt diff --git a/scripts/shukaro/readme.md b/scripts/shukaro/readme.md index 7b01dfc..6cb98d8 100644 --- a/scripts/shukaro/readme.md +++ b/scripts/shukaro/readme.md @@ -1,7 +1,7 @@ # Creation Forge Created by Shukaro. (http://www.bay12forums.com/smf/index.php?topic=60815) -Forge ported to PyDwarf by Sophie Kirschner. (http://www.pineapplemachine.com) +Rewritten for PyDwarf by Sophie Kirschner. (http://www.pineapplemachine.com) This is a simple workshop I modded in to help test custom reactions, buildings, and creatures. It's used to create various different items so that you don't have to set up an entire fortress to test some reactions. Hopefully it's a useful tool to people, even if it's just to look at the raw formatting. @@ -9,7 +9,7 @@ This is a simple workshop I modded in to help test custom reactions, buildings, # Dwarven Higher Learning Created by Shukaro. (http://www.bay12forums.com/smf/index.php?topic=60853.0) -Ported to PyDwarf by Sophie Kirschner. (http://www.pineapplemachine.com) +Rewritten for PyDwarf by Sophie Kirschner. (http://www.pineapplemachine.com) ## Introduction diff --git a/scripts/shukaro/shukaroutils.py b/scripts/shukaro/shukaroutils.py deleted file mode 100644 index 2994c1a..0000000 --- a/scripts/shukaro/shukaroutils.py +++ /dev/null @@ -1,40 +0,0 @@ -# Defines functions common to shukaro mods - -import raws - -default_entities = ('MOUNTAIN',) - -def getentities(dfraws, entities): - return dfraws.all(exact_value='ENTITY', re_args=('|'.join(entities),)) - -def addraws(pydwarf, dfraws, rawsdir, entities, extratokens=None): - # Get the entities that need to have permitted things added to them. - entitytokens = getentities(dfraws, entities) - if len(entitytokens) != len(entities): - if len(entitytokens): - pydwarf.log.error('Of entities %s passed by argument, only %s were found.' % (entities, entitytokens)) - else: - return pydwarf.failure('Found none of entities %s to which to add permitted buildings and reactions.' % entities) - - # Read the raws - try: - shukaroraws = raws.dir(path=rawsdir, log=pydwarf.log) - except: - pydwarf.log.exception('Failed to load raws from %s.' % rawsdir) - return pydwarf.failure('Failed to load raws from %s.' % rawsdir) - - # Add new buildings and reactions to entities, and whatever else needs adding - buildings = shukaroraws.all(exact_value='BUILDING_WORKSHOP', args_count=1) - reactions = shukaroraws.all(exact_value='REACTION', args_count=1) - for entity in entitytokens: - if extratokens: entity.add(extratokens) - for building in buildings: entity.add(raws.token(value='PERMITTED_BUILDING', args=(building.args[0],))) - for reaction in reactions: entity.add(raws.token(value='PERMITTED_REACTION', args=(reaction.args[0],))) - pydwarf.log.info('Added %d permitted buildings and %d permitted reactions to %d entities.' % (len(buildings), len(reactions), len(entitytokens))) - - # Add new files - for filename, rfile in shukaroraws.files.iteritems(): - if filename not in dfraws: dfraws.addfile(rfile=rfile) - - # All done! - return pydwarf.success() diff --git a/scripts/smeeprocket/readme.md b/scripts/smeeprocket/readme.md new file mode 100644 index 0000000..8f7174e --- /dev/null +++ b/scripts/smeeprocket/readme.md @@ -0,0 +1,6 @@ +# Transgender and Intersex + +Created by SmeepRocket. (http://www.bay12forums.com/smf/index.php?topic=146312.0) +Rewritten for PyDwarf by Sophie Kirschner. (http://www.pineapplemachine.com) + +This adds transmales, transfemales, intersex males, and intersex females to the dwarf fortress fortress mode game. This does not add it to adventure mode. diff --git a/scripts/smeeprocket/readme.txt b/scripts/smeeprocket/readme.txt deleted file mode 100644 index 19ae8f6..0000000 --- a/scripts/smeeprocket/readme.txt +++ /dev/null @@ -1,4 +0,0 @@ -Transgender and Intersex mod created by SmeepRocket. (http://www.bay12forums.com/smf/index.php?topic=146312.0) -Transgender and Intersex mod ported to PyDwarf by Sophie Kirschner. (http://www.pineapplemachine.com) - -This adds transmales, transfemales, intersex males, and intersex females to the dwarf fortress fortress mode game. This does not add it to adventure mode. diff --git a/scripts/stal/armouryentities.json b/scripts/stal/armouryentities.json index 78d33d0..e09a117 100644 --- a/scripts/stal/armouryentities.json +++ b/scripts/stal/armouryentities.json @@ -1,483 +1,1318 @@ { "MOUNTAIN": { "SHOES": [ - "ITEM_SHOES_CHAUSSE_CHAIN", - "ITEM_SHOES_GREAVES_LOWER", - "ITEM_SHOES_BOOTS", - "ITEM_SHOES_SHOES", - "ITEM_SHOES_ANKLE", - "ITEM_SHOES_CARBATINAE", - "ITEM_SHOES_SOCKS" - ], + [ + "ITEM_SHOES_CHAUSSE_CHAIN", + "COMMON" + ], + [ + "ITEM_SHOES_GREAVES_LOWER", + "COMMON" + ], + [ + "ITEM_SHOES_BOOTS", + "COMMON" + ], + [ + "ITEM_SHOES_SHOES", + "FORCED" + ], + [ + "ITEM_SHOES_ANKLE", + "COMMON" + ], + [ + "ITEM_SHOES_CARBATINAE", + "COMMON" + ], + [ + "ITEM_SHOES_SOCKS", + "FORCED" + ] + ], "ARMOR": [ - "ITEM_ARMOR_PLATE", - "ITEM_ARMOR_HAUBERK", - "ITEM_ARMOR_LAMELLAR", - "ITEM_ARMOR_JERKIN", - "ITEM_ARMOR_GAMBESON", - "ITEM_ARMOR_CLOAK", - "ITEM_ARMOR_CAPE", - "ITEM_ARMOR_COAT", - "ITEM_ARMOR_VEST", - "ITEM_ARMOR_DRESS", - "ITEM_ARMOR_ROBE", - "ITEM_ARMOR_SHIRT", - "ITEM_ARMOR_TUNIC" - ], + [ + "ITEM_ARMOR_PLATE", + "COMMON" + ], + [ + "ITEM_ARMOR_HAUBERK", + "COMMON" + ], + [ + "ITEM_ARMOR_LAMELLAR", + "COMMON" + ], + [ + "ITEM_ARMOR_JERKIN", + "COMMON" + ], + [ + "ITEM_ARMOR_GAMBESON", + "COMMON" + ], + [ + "ITEM_ARMOR_CLOAK", + "COMMON" + ], + [ + "ITEM_ARMOR_CAPE", + "COMMON" + ], + [ + "ITEM_ARMOR_COAT", + "COMMON" + ], + [ + "ITEM_ARMOR_VEST", + "COMMON" + ], + [ + "ITEM_ARMOR_DRESS", + "COMMON" + ], + [ + "ITEM_ARMOR_ROBE", + "COMMON" + ], + [ + "ITEM_ARMOR_SHIRT", + "COMMON" + ], + [ + "ITEM_ARMOR_TUNIC", + "COMMON" + ] + ], "WEAPON": [ - "ITEM_WEAPON_SWORD_ARMING_SHORT", - "ITEM_WEAPON_SWORD_FALCHION", - "ITEM_WEAPON_DAGGER_FIGHTING", - "ITEM_WEAPON_AXE_HATCHET", - "ITEM_WEAPON_AXE_BATTLE", - "ITEM_WEAPON_AXE_HAMMER", - "ITEM_WEAPON_MACE_GOEDENDAG", - "ITEM_WEAPON_MACE_FLAIL", - "ITEM_WEAPON_MACE_FLAIL_SPIKED", - "ITEM_WEAPON_MACE_FLAIL_2H", - "ITEM_WEAPON_MACE_PERNACH", - "ITEM_WEAPON_HAMMER_WAR", - "ITEM_WEAPON_HAMMER_PICK", - "ITEM_WEAPON_HAMMER_MAUL", - "ITEM_WEAPON_SPEAR", - "ITEM_WEAPON_SPEAR_BOAR", - "ITEM_WEAPON_AXE_BARDICHE", - "ITEM_WEAPON_CROSSBOW", - "ITEM_WEAPON_CROSSBOW_BAYONET", - "ITEM_WEAPON_CROSSBOW_SIEGE", - "ITEM_WEAPON_AXE_TRAINING", - "ITEM_WEAPON_SWORD_TRAINING", - "ITEM_WEAPON_SPEAR_TRAINING", - "ITEM_WEAPON_HAMMER_TRAINING", - "ITEM_WEAPON_MACE_TRAINING" - ], + [ + "ITEM_WEAPON_SWORD_ARMING_SHORT" + ], + [ + "ITEM_WEAPON_SWORD_FALCHION" + ], + [ + "ITEM_WEAPON_DAGGER_FIGHTING" + ], + [ + "ITEM_WEAPON_AXE_HATCHET" + ], + [ + "ITEM_WEAPON_AXE_BATTLE" + ], + [ + "ITEM_WEAPON_AXE_HAMMER" + ], + [ + "ITEM_WEAPON_MACE_GOEDENDAG" + ], + [ + "ITEM_WEAPON_MACE_FLAIL" + ], + [ + "ITEM_WEAPON_MACE_FLAIL_SPIKED" + ], + [ + "ITEM_WEAPON_MACE_FLAIL_2H" + ], + [ + "ITEM_WEAPON_MACE_PERNACH" + ], + [ + "ITEM_WEAPON_HAMMER_WAR" + ], + [ + "ITEM_WEAPON_HAMMER_PICK" + ], + [ + "ITEM_WEAPON_HAMMER_MAUL" + ], + [ + "ITEM_WEAPON_SPEAR" + ], + [ + "ITEM_WEAPON_SPEAR_BOAR" + ], + [ + "ITEM_WEAPON_AXE_BARDICHE" + ], + [ + "ITEM_WEAPON_CROSSBOW" + ], + [ + "ITEM_WEAPON_CROSSBOW_BAYONET" + ], + [ + "ITEM_WEAPON_CROSSBOW_SIEGE" + ], + [ + "ITEM_WEAPON_AXE_TRAINING" + ], + [ + "ITEM_WEAPON_SWORD_TRAINING" + ], + [ + "ITEM_WEAPON_SPEAR_TRAINING" + ], + [ + "ITEM_WEAPON_HAMMER_TRAINING" + ], + [ + "ITEM_WEAPON_MACE_TRAINING" + ] + ], "SHIELD": [ - "ITEM_SHIELD_KITE", - "ITEM_SHIELD_TOWER" - ], + [ + "ITEM_SHIELD_KITE" + ], + [ + "ITEM_SHIELD_TOWER" + ] + ], "GLOVES": [ - "ITEM_GLOVES_MITTENS", - "ITEM_GLOVES_GAUNTLETS", - "ITEM_GLOVES_GLOVES", - "ITEM_GLOVES_MITTENS_CHAIN", - "ITEM_GLOVES_GLOVES_CHAIN", - "ITEM_GLOVES_WRAP" - ], + [ + "ITEM_GLOVES_MITTENS", + "COMMON" + ], + [ + "ITEM_GLOVES_GAUNTLETS", + "COMMON" + ], + [ + "ITEM_GLOVES_GLOVES", + "COMMON" + ], + [ + "ITEM_GLOVES_MITTENS_CHAIN", + "COMMON" + ], + [ + "ITEM_GLOVES_GLOVES_CHAIN", + "COMMON" + ], + [ + "ITEM_GLOVES_WRAP", + "FORCED" + ] + ], "HELM": [ - "ITEM_HELM_GREAT", - "ITEM_HELM_KETTLE", - "ITEM_HELM_BASCINET", - "ITEM_HELM_BARBUTE", - "ITEM_HELM_SALLET", - "ITEM_HELM_SKULLCAP", - "ITEM_HELM_COIF", - "ITEM_HELM_CAP", - "ITEM_HELM_CAP_ARMING", - "ITEM_HELM_HOOD" - ], + [ + "ITEM_HELM_GREAT", + "COMMON" + ], + [ + "ITEM_HELM_KETTLE", + "COMMON" + ], + [ + "ITEM_HELM_BASCINET", + "COMMON" + ], + [ + "ITEM_HELM_BARBUTE", + "COMMON" + ], + [ + "ITEM_HELM_SALLET", + "COMMON" + ], + [ + "ITEM_HELM_SKULLCAP", + "COMMON" + ], + [ + "ITEM_HELM_COIF", + "COMMON" + ], + [ + "ITEM_HELM_CAP", + "COMMON" + ], + [ + "ITEM_HELM_CAP_ARMING", + "COMMON" + ], + [ + "ITEM_HELM_HOOD", + "COMMON" + ] + ], "DIGGER": [ - "ITEM_WEAPON_PICK" - ], + [ + "ITEM_WEAPON_PICK" + ] + ], "TOOL": [ - "ITEM_TOOL_NEST_BOX", - "ITEM_TOOL_JUG", - "ITEM_TOOL_LARGE_POT", - "ITEM_TOOL_HIVE", - "ITEM_TOOL_MINECART", - "ITEM_TOOL_WHEELBARROW", - "ITEM_TOOL_STEPLADDER" - ], - "AMMO": { - "ITEM_WEAPON_CROSSBOW_SIEGE": [ - "ITEM_AMMO_QUARREL" - ], - "ITEM_WEAPON_CROSSBOW_BAYONET": [ + [ + "ITEM_TOOL_NEST_BOX" + ], + [ + "ITEM_TOOL_JUG" + ], + [ + "ITEM_TOOL_LARGE_POT" + ], + [ + "ITEM_TOOL_HIVE" + ], + [ + "ITEM_TOOL_MINECART" + ], + [ + "ITEM_TOOL_WHEELBARROW" + ], + [ + "ITEM_TOOL_STEPLADDER" + ] + ], + "AMMO": [ + [ "ITEM_AMMO_BOLTS_BARB" - ], - "ITEM_WEAPON_CROSSBOW": [ + ], + [ "ITEM_AMMO_BOLTS_BARB" + ], + [ + "ITEM_AMMO_QUARREL" ] - }, + ], "PANTS": [ - "ITEM_PANTS_GREAVES_UPPER", - "ITEM_PANTS_BRACCAE", - "ITEM_PANTS_PANTS", - "ITEM_PANTS_CHAUSSE", - "ITEM_PANTS_CHAUSSE_CHAIN", - "ITEM_PANTS_SKIRT", - "ITEM_PANTS_BRAIES", - "ITEM_PANTS_LOINCLOTH" + [ + "ITEM_PANTS_GREAVES_UPPER", + "COMMON" + ], + [ + "ITEM_PANTS_BRACCAE", + "COMMON" + ], + [ + "ITEM_PANTS_PANTS", + "COMMON" + ], + [ + "ITEM_PANTS_CHAUSSE", + "COMMON" + ], + [ + "ITEM_PANTS_CHAUSSE_CHAIN", + "COMMON" + ], + [ + "ITEM_PANTS_SKIRT", + "COMMON" + ], + [ + "ITEM_PANTS_BRAIES", + "COMMON" + ], + [ + "ITEM_PANTS_LOINCLOTH", + "FORCED" + ] ] - }, + }, "SKULKING": { "SHOES": [ - "ITEM_SHOES_SOCKS" - ], + [ + "ITEM_SHOES_SOCKS", + "FORCED" + ] + ], "ARMOR": [ - "ITEM_ARMOR_TUNIC" - ], + [ + "ITEM_ARMOR_TUNIC", + "FORCED" + ] + ], "WEAPON": [ - "ITEM_WEAPON_SEAX_LONG", - "ITEM_WEAPON_SEAX_NARROW", - "ITEM_WEAPON_SEAX_BROAD", - "ITEM_WEAPON_SEAX_SHORT", - "ITEM_WEAPON_SPEAR", - "ITEM_WEAPON_SLING_STAFF", - "ITEM_WEAPON_ATLATL", - "ITEM_WEAPON_SWORD_TRAINING", - "ITEM_WEAPON_SPEAR_TRAINING" - ], + [ + "ITEM_WEAPON_SEAX_LONG" + ], + [ + "ITEM_WEAPON_SEAX_NARROW" + ], + [ + "ITEM_WEAPON_SEAX_BROAD" + ], + [ + "ITEM_WEAPON_SEAX_SHORT" + ], + [ + "ITEM_WEAPON_SPEAR" + ], + [ + "ITEM_WEAPON_SLING_STAFF" + ], + [ + "ITEM_WEAPON_ATLATL" + ], + [ + "ITEM_WEAPON_SWORD_TRAINING" + ], + [ + "ITEM_WEAPON_SPEAR_TRAINING" + ] + ], "GLOVES": [ - "ITEM_GLOVES_WRAP" - ], + [ + "ITEM_GLOVES_WRAP", + "FORCED" + ] + ], "HELM": [ - "ITEM_HELM_HOOD", - "ITEM_HELM_CAP" - ], - "AMMO": { - "ITEM_WEAPON_ATLATL": [ - "ITEM_AMMO_DARTS" - ], - "ITEM_WEAPON_SLING_STAFF": [ + [ + "ITEM_HELM_HOOD", + "FORCED" + ], + [ + "ITEM_HELM_CAP", + "FORCED" + ] + ], + "AMMO": [ + [ "ITEM_AMMO_BULLET" + ], + [ + "ITEM_AMMO_DARTS" ] - }, + ], "PANTS": [ - "ITEM_PANTS_BRACCAE", - "ITEM_PANTS_LOINCLOTH" + [ + "ITEM_PANTS_BRACCAE", + "FORCED" + ], + [ + "ITEM_PANTS_LOINCLOTH", + "FORCED" + ] ] - }, + }, "EVIL": { "SHOES": [ - "ITEM_SHOES_CHAUSSE_CHAIN", - "ITEM_SHOES_GREAVES_LOWER", - "ITEM_SHOES_BOOTS", - "ITEM_SHOES_SHOES", - "ITEM_SHOES_ANKLE", - "ITEM_SHOES_CARBATINAE", - "ITEM_SHOES_SANDALS", - "ITEM_SHOES_SOCKS" - ], + [ + "ITEM_SHOES_CHAUSSE_CHAIN", + "COMMON" + ], + [ + "ITEM_SHOES_GREAVES_LOWER", + "UNCOMMON" + ], + [ + "ITEM_SHOES_BOOTS", + "COMMON" + ], + [ + "ITEM_SHOES_SHOES", + "COMMON" + ], + [ + "ITEM_SHOES_ANKLE", + "COMMON" + ], + [ + "ITEM_SHOES_CARBATINAE", + "FORCED" + ], + [ + "ITEM_SHOES_SANDALS", + "COMMON" + ], + [ + "ITEM_SHOES_SOCKS", + "FORCED" + ] + ], "ARMOR": [ - "ITEM_ARMOR_PLATE", - "ITEM_ARMOR_HAUBERK", - "ITEM_ARMOR_JERKIN", - "ITEM_ARMOR_ROBE", - "ITEM_ARMOR_SHIRT", - "ITEM_ARMOR_TUNIC", - "ITEM_ARMOR_CLOAK" - ], + [ + "ITEM_ARMOR_PLATE", + "UNCOMMON" + ], + [ + "ITEM_ARMOR_HAUBERK", + "COMMON" + ], + [ + "ITEM_ARMOR_JERKIN", + "COMMON" + ], + [ + "ITEM_ARMOR_ROBE", + "COMMON" + ], + [ + "ITEM_ARMOR_SHIRT", + "COMMON" + ], + [ + "ITEM_ARMOR_TUNIC", + "COMMON" + ], + [ + "ITEM_ARMOR_CLOAK", + "COMMON" + ] + ], "WEAPON": [ - "ITEM_WEAPON_SEAX_LONG", - "ITEM_WEAPON_SEAX_NARROW", - "ITEM_WEAPON_SEAX_BROAD", - "ITEM_WEAPON_SEAX_SHORT", - "ITEM_WEAPON_AXE_HATCHET", - "ITEM_WEAPON_SPEAR", - "ITEM_WEAPON_WHIP", - "ITEM_WEAPON_WHIP_HOOKED", - "ITEM_WEAPON_WHIP_KNOTTED", - "ITEM_WEAPON_WHIP_SCOURGE", - "ITEM_WEAPON_MACE_KNOBBED", - "ITEM_WEAPON_MACE_FLAIL", - "ITEM_WEAPON_MACE_FLAIL_SPIKED", - "ITEM_WEAPON_MACE_FLAIL_2H", - "ITEM_WEAPON_HAMMER_MAUL", - "ITEM_WEAPON_SLING_STAFF", - "ITEM_WEAPON_ATLATL", - "ITEM_WEAPON_SWORD_TRAINING", - "ITEM_WEAPON_SPEAR_TRAINING", - "ITEM_WEAPON_AXE_TRAINING", - "ITEM_WEAPON_MACE_TRAINING" - ], + [ + "ITEM_WEAPON_SEAX_LONG" + ], + [ + "ITEM_WEAPON_SEAX_NARROW" + ], + [ + "ITEM_WEAPON_SEAX_BROAD" + ], + [ + "ITEM_WEAPON_SEAX_SHORT" + ], + [ + "ITEM_WEAPON_AXE_HATCHET" + ], + [ + "ITEM_WEAPON_SPEAR" + ], + [ + "ITEM_WEAPON_WHIP" + ], + [ + "ITEM_WEAPON_WHIP_HOOKED" + ], + [ + "ITEM_WEAPON_WHIP_KNOTTED" + ], + [ + "ITEM_WEAPON_WHIP_SCOURGE" + ], + [ + "ITEM_WEAPON_MACE_KNOBBED" + ], + [ + "ITEM_WEAPON_MACE_FLAIL" + ], + [ + "ITEM_WEAPON_MACE_FLAIL_SPIKED" + ], + [ + "ITEM_WEAPON_MACE_FLAIL_2H" + ], + [ + "ITEM_WEAPON_HAMMER_MAUL" + ], + [ + "ITEM_WEAPON_SLING_STAFF" + ], + [ + "ITEM_WEAPON_ATLATL" + ], + [ + "ITEM_WEAPON_SWORD_TRAINING" + ], + [ + "ITEM_WEAPON_SPEAR_TRAINING" + ], + [ + "ITEM_WEAPON_AXE_TRAINING" + ], + [ + "ITEM_WEAPON_MACE_TRAINING" + ] + ], "SHIELD": [ - "ITEM_SHIELD_SHIELD", - "ITEM_SHIELD_KITE" - ], + [ + "ITEM_SHIELD_SHIELD" + ], + [ + "ITEM_SHIELD_KITE" + ] + ], "GLOVES": [ - "ITEM_GLOVES_MITTENS", - "ITEM_GLOVES_GAUNTLETS", - "ITEM_GLOVES_GLOVES", - "ITEM_GLOVES_MITTENS_CHAIN", - "ITEM_GLOVES_GLOVES_CHAIN", - "ITEM_GLOVES_WRAP" - ], + [ + "ITEM_GLOVES_MITTENS", + "COMMON" + ], + [ + "ITEM_GLOVES_GAUNTLETS", + "UNCOMMON" + ], + [ + "ITEM_GLOVES_GLOVES", + "COMMON" + ], + [ + "ITEM_GLOVES_MITTENS_CHAIN", + "COMMON" + ], + [ + "ITEM_GLOVES_GLOVES_CHAIN", + "COMMON" + ], + [ + "ITEM_GLOVES_WRAP", + "FORCED" + ] + ], "HELM": [ - "ITEM_HELM_BARBUTE", - "ITEM_HELM_SALLET", - "ITEM_HELM_SKULLCAP", - "ITEM_HELM_CAP", - "ITEM_HELM_CAP_ARMING", - "ITEM_HELM_HOOD", - "ITEM_HELM_SCARF_HEAD" - ], + [ + "ITEM_HELM_BARBUTE", + "COMMON" + ], + [ + "ITEM_HELM_SALLET", + "COMMON" + ], + [ + "ITEM_HELM_SKULLCAP", + "COMMON" + ], + [ + "ITEM_HELM_CAP", + "COMMON" + ], + [ + "ITEM_HELM_CAP_ARMING", + "COMMON" + ], + [ + "ITEM_HELM_HOOD", + "COMMON" + ], + [ + "ITEM_HELM_SCARF_HEAD", + "COMMON" + ] + ], "DIGGER": [ - "ITEM_WEAPON_PICK" - ], - "AMMO": { - "ITEM_WEAPON_ATLATL": [ - "ITEM_AMMO_DARTS" - ], - "ITEM_WEAPON_SLING_STAFF": [ + [ + "ITEM_WEAPON_PICK" + ] + ], + "AMMO": [ + [ "ITEM_AMMO_BULLET" + ], + [ + "ITEM_AMMO_DARTS" ] - }, + ], "PANTS": [ - "ITEM_PANTS_BRACCAE", - "ITEM_PANTS_LOINCLOTH", - "ITEM_PANTS_PANTS" + [ + "ITEM_PANTS_BRACCAE", + "COMMON" + ], + [ + "ITEM_PANTS_LOINCLOTH", + "FORCED" + ], + [ + "ITEM_PANTS_PANTS", + "COMMON" + ] ] - }, + }, "SUBTERRANEAN_ANIMAL_PEOPLES": { "WEAPON": [ - "ITEM_WEAPON_SPEAR", - "ITEM_WEAPON_BLOWGUN", - "ITEM_WEAPON_ATLATL" - ], + [ + "ITEM_WEAPON_SPEAR" + ], + [ + "ITEM_WEAPON_BLOWGUN" + ], + [ + "ITEM_WEAPON_ATLATL" + ] + ], "SHIELD": [ - "ITEM_SHIELD_BUCKLER" - ], - "AMMO": { - "ITEM_WEAPON_ATLATL": [ - "ITEM_AMMO_DARTS" - ], - "ITEM_WEAPON_BLOWGUN": [ + [ + "ITEM_SHIELD_BUCKLER" + ] + ], + "AMMO": [ + [ "ITEM_AMMO_BLOWDARTS" + ], + [ + "ITEM_AMMO_DARTS" ] - } - }, + ] + }, "PLAINS": { "SHOES": [ - "ITEM_SHOES_SHOES", - "ITEM_SHOES_CHAUSSE_CHAIN", - "ITEM_SHOES_GREAVES_LOWER", - "ITEM_SHOES_ANKLE", - "ITEM_SHOES_BOOTS", - "ITEM_SHOES_SOCKS" - ], + [ + "ITEM_SHOES_SHOES", + "COMMON" + ], + [ + "ITEM_SHOES_CHAUSSE_CHAIN", + "COMMON" + ], + [ + "ITEM_SHOES_GREAVES_LOWER", + "COMMON" + ], + [ + "ITEM_SHOES_ANKLE", + "FORCED" + ], + [ + "ITEM_SHOES_BOOTS", + "COMMON" + ], + [ + "ITEM_SHOES_SOCKS", + "FORCED" + ] + ], "ARMOR": [ - "ITEM_ARMOR_PLATE", - "ITEM_ARMOR_HAUBERK", - "ITEM_ARMOR_HAUBERGEON", - "ITEM_ARMOR_BRIGANDINE", - "ITEM_ARMOR_BYRNIE", - "ITEM_ARMOR_LAMELLAR", - "ITEM_ARMOR_CLOAK", - "ITEM_ARMOR_SURCOAT", - "ITEM_ARMOR_TABARD", - "ITEM_ARMOR_CAPE", - "ITEM_ARMOR_COAT", - "ITEM_ARMOR_VEST", - "ITEM_ARMOR_DRESS", - "ITEM_ARMOR_ROBE", - "ITEM_ARMOR_SHIRT", - "ITEM_ARMOR_TUNIC", - "ITEM_ARMOR_JERKIN", - "ITEM_ARMOR_AKETON", - "ITEM_ARMOR_GAMBESON" - ], + [ + "ITEM_ARMOR_PLATE", + "COMMON" + ], + [ + "ITEM_ARMOR_HAUBERK", + "COMMON" + ], + [ + "ITEM_ARMOR_HAUBERGEON", + "COMMON" + ], + [ + "ITEM_ARMOR_BRIGANDINE", + "COMMON" + ], + [ + "ITEM_ARMOR_BYRNIE", + "COMMON" + ], + [ + "ITEM_ARMOR_LAMELLAR", + "COMMON" + ], + [ + "ITEM_ARMOR_CLOAK", + "COMMON" + ], + [ + "ITEM_ARMOR_SURCOAT", + "COMMON" + ], + [ + "ITEM_ARMOR_TABARD", + "COMMON" + ], + [ + "ITEM_ARMOR_CAPE", + "COMMON" + ], + [ + "ITEM_ARMOR_COAT", + "COMMON" + ], + [ + "ITEM_ARMOR_VEST", + "COMMON" + ], + [ + "ITEM_ARMOR_DRESS", + "COMMON" + ], + [ + "ITEM_ARMOR_ROBE", + "COMMON" + ], + [ + "ITEM_ARMOR_SHIRT", + "COMMON" + ], + [ + "ITEM_ARMOR_TUNIC", + "COMMON" + ], + [ + "ITEM_ARMOR_JERKIN", + "COMMON" + ], + [ + "ITEM_ARMOR_AKETON", + "COMMON" + ], + [ + "ITEM_ARMOR_GAMBESON", + "COMMON" + ] + ], "WEAPON": [ - "ITEM_WEAPON_SEAX_LONG", - "ITEM_WEAPON_SEAX_NARROW", - "ITEM_WEAPON_SEAX_BROAD", - "ITEM_WEAPON_SEAX_SHORT", - "ITEM_WEAPON_DAGGER_FIGHTING", - "ITEM_WEAPON_DAGGER_RONDEL", - "ITEM_WEAPON_SWORD_RAIDER", - "ITEM_WEAPON_SWORD_ARMING_SHORT", - "ITEM_WEAPON_SWORD_ARMING", - "ITEM_WEAPON_SWORD_ARMING_LONG", - "ITEM_WEAPON_LONGSWORD", - "ITEM_WEAPON_SWORD_GREAT", - "ITEM_WEAPON_SWORD_2H", - "ITEM_WEAPON_SWORD_FALCHION", - "ITEM_WEAPON_AXE_HATCHET", - "ITEM_WEAPON_AXE_RAIDER", - "ITEM_WEAPON_AXE_HAFTED", - "ITEM_WEAPON_MACE", - "ITEM_WEAPON_MACE_MORNINGSTAR", - "ITEM_WEAPON_MACE_FLAIL", - "ITEM_WEAPON_MACE_FLAIL_SPIKED", - "ITEM_WEAPON_MACE_FLANGED", - "ITEM_WEAPON_MACE_FLAIL", - "ITEM_WEAPON_MACE_FLAIL_2H", - "ITEM_WEAPON_MACE_KNOBBED", - "ITEM_WEAPON_MACE_PERNACH", - "ITEM_WEAPON_MACE_GOEDENDAG", - "ITEM_WEAPON_HAMMER_WAR", - "ITEM_WEAPON_HAMMER_HAFTED", - "ITEM_WEAPON_SPEAR", - "ITEM_WEAPON_PIKE_AWLPIKE", - "ITEM_WEAPON_SPEAR_SPETUM", - "ITEM_WEAPON_SPEAR_BOAR", - "ITEM_WEAPON_SPEAR_HEWING", - "ITEM_WEAPON_PIKE_PIKE", - "ITEM_WEAPON_PIKE_SWORD", - "ITEM_WEAPON_PIKE_GUISARME", - "ITEM_WEAPON_PIKE_HALBERD", - "ITEM_WEAPON_PIKE_VOULGE", - "ITEM_WEAPON_CROSSBOW", - "ITEM_WEAPON_LONGBOW", - "ITEM_WEAPON_ATLATL", - "ITEM_WEAPON_AXE_TRAINING", - "ITEM_WEAPON_SWORD_TRAINING", - "ITEM_WEAPON_SPEAR_TRAINING", - "ITEM_WEAPON_PIKE_TRAINING", - "ITEM_WEAPON_MACE_TRAINING", - "ITEM_WEAPON_HAMMER_TRAINING" - ], + [ + "ITEM_WEAPON_SEAX_LONG" + ], + [ + "ITEM_WEAPON_SEAX_NARROW" + ], + [ + "ITEM_WEAPON_SEAX_BROAD" + ], + [ + "ITEM_WEAPON_SEAX_SHORT" + ], + [ + "ITEM_WEAPON_DAGGER_FIGHTING" + ], + [ + "ITEM_WEAPON_DAGGER_RONDEL" + ], + [ + "ITEM_WEAPON_SWORD_RAIDER" + ], + [ + "ITEM_WEAPON_SWORD_ARMING_SHORT" + ], + [ + "ITEM_WEAPON_SWORD_ARMING" + ], + [ + "ITEM_WEAPON_SWORD_ARMING_LONG" + ], + [ + "ITEM_WEAPON_LONGSWORD" + ], + [ + "ITEM_WEAPON_SWORD_GREAT" + ], + [ + "ITEM_WEAPON_SWORD_2H" + ], + [ + "ITEM_WEAPON_SWORD_FALCHION" + ], + [ + "ITEM_WEAPON_AXE_HATCHET" + ], + [ + "ITEM_WEAPON_AXE_RAIDER" + ], + [ + "ITEM_WEAPON_AXE_HAFTED" + ], + [ + "ITEM_WEAPON_MACE" + ], + [ + "ITEM_WEAPON_MACE_MORNINGSTAR" + ], + [ + "ITEM_WEAPON_MACE_FLAIL" + ], + [ + "ITEM_WEAPON_MACE_FLAIL_SPIKED" + ], + [ + "ITEM_WEAPON_MACE_FLANGED" + ], + [ + "ITEM_WEAPON_MACE_FLAIL" + ], + [ + "ITEM_WEAPON_MACE_FLAIL_2H" + ], + [ + "ITEM_WEAPON_MACE_KNOBBED" + ], + [ + "ITEM_WEAPON_MACE_PERNACH" + ], + [ + "ITEM_WEAPON_MACE_GOEDENDAG" + ], + [ + "ITEM_WEAPON_HAMMER_WAR" + ], + [ + "ITEM_WEAPON_HAMMER_HAFTED" + ], + [ + "ITEM_WEAPON_SPEAR" + ], + [ + "ITEM_WEAPON_PIKE_AWLPIKE" + ], + [ + "ITEM_WEAPON_SPEAR_SPETUM" + ], + [ + "ITEM_WEAPON_SPEAR_BOAR" + ], + [ + "ITEM_WEAPON_SPEAR_HEWING" + ], + [ + "ITEM_WEAPON_PIKE_PIKE" + ], + [ + "ITEM_WEAPON_PIKE_SWORD" + ], + [ + "ITEM_WEAPON_PIKE_GUISARME" + ], + [ + "ITEM_WEAPON_PIKE_HALBERD" + ], + [ + "ITEM_WEAPON_PIKE_VOULGE" + ], + [ + "ITEM_WEAPON_CROSSBOW" + ], + [ + "ITEM_WEAPON_LONGBOW" + ], + [ + "ITEM_WEAPON_ATLATL" + ], + [ + "ITEM_WEAPON_AXE_TRAINING" + ], + [ + "ITEM_WEAPON_SWORD_TRAINING" + ], + [ + "ITEM_WEAPON_SPEAR_TRAINING" + ], + [ + "ITEM_WEAPON_PIKE_TRAINING" + ], + [ + "ITEM_WEAPON_MACE_TRAINING" + ], + [ + "ITEM_WEAPON_HAMMER_TRAINING" + ] + ], "SHIELD": [ - "ITEM_SHIELD_BUCKLER", - "ITEM_SHIELD_HEATER" - ], + [ + "ITEM_SHIELD_BUCKLER" + ], + [ + "ITEM_SHIELD_HEATER" + ] + ], "GLOVES": [ - "ITEM_GLOVES_MITTENS", - "ITEM_GLOVES_GAUNTLETS", - "ITEM_GLOVES_GLOVES", - "ITEM_GLOVES_MITTENS_CHAIN", - "ITEM_GLOVES_GLOVES_CHAIN", - "ITEM_GLOVES_WRAP" - ], + [ + "ITEM_GLOVES_MITTENS", + "COMMOND" + ], + [ + "ITEM_GLOVES_GAUNTLETS", + "COMMON" + ], + [ + "ITEM_GLOVES_GLOVES", + "COMMON" + ], + [ + "ITEM_GLOVES_MITTENS_CHAIN", + "COMMON" + ], + [ + "ITEM_GLOVES_GLOVES_CHAIN", + "COMMON" + ], + [ + "ITEM_GLOVES_WRAP", + "FORCED" + ] + ], "HELM": [ - "ITEM_HELM_GREAT", - "ITEM_HELM_NASAL", - "ITEM_HELM_KETTLE", - "ITEM_HELM_BASCINET", - "ITEM_HELM_SALLET", - "ITEM_HELM_SKULLCAP", - "ITEM_HELM_COIF", - "ITEM_HELM_COIF_PADDED", - "ITEM_HELM_RAIDER", - "ITEM_HELM_CAP", - "ITEM_HELM_CAP_ARMING", - "ITEM_HELM_HOOD" - ], + [ + "ITEM_HELM_GREAT", + "COMMON" + ], + [ + "ITEM_HELM_NASAL", + "COMMON" + ], + [ + "ITEM_HELM_KETTLE", + "COMMON" + ], + [ + "ITEM_HELM_BASCINET", + "COMMON" + ], + [ + "ITEM_HELM_SALLET", + "COMMON" + ], + [ + "ITEM_HELM_SKULLCAP", + "COMMON" + ], + [ + "ITEM_HELM_COIF", + "COMMON" + ], + [ + "ITEM_HELM_COIF_PADDED", + "COMMON" + ], + [ + "ITEM_HELM_RAIDER", + "COMMON" + ], + [ + "ITEM_HELM_CAP", + "COMMON" + ], + [ + "ITEM_HELM_CAP_ARMING", + "COMMON" + ], + [ + "ITEM_HELM_HOOD", + "COMMON" + ] + ], "DIGGER": [ - "ITEM_WEAPON_PICK" - ], + [ + "ITEM_WEAPON_PICK" + ] + ], "TOOL": [ - "ITEM_TOOL_CAULDRON", - "ITEM_TOOL_LADLE", - "ITEM_TOOL_BOWL", - "ITEM_TOOL_MORTAR", - "ITEM_TOOL_PESTLE", - "ITEM_TOOL_KNIFE_CARVING", - "ITEM_TOOL_KNIFE_BONING", - "ITEM_TOOL_KNIFE_SLICING", - "ITEM_TOOL_KNIFE_MEAT_CLEAVER", - "ITEM_TOOL_FORK_CARVING", - "ITEM_TOOL_NEST_BOX", - "ITEM_TOOL_JUG", - "ITEM_TOOL_LARGE_POT", - "ITEM_TOOL_HIVE", - "ITEM_TOOL_POUCH", - "ITEM_TOOL_WHEELBARROW" - ], - "AMMO": { - "ITEM_WEAPON_CROSSBOW": [ + [ + "ITEM_TOOL_CAULDRON" + ], + [ + "ITEM_TOOL_LADLE" + ], + [ + "ITEM_TOOL_BOWL" + ], + [ + "ITEM_TOOL_MORTAR" + ], + [ + "ITEM_TOOL_PESTLE" + ], + [ + "ITEM_TOOL_KNIFE_CARVING" + ], + [ + "ITEM_TOOL_KNIFE_BONING" + ], + [ + "ITEM_TOOL_KNIFE_SLICING" + ], + [ + "ITEM_TOOL_KNIFE_MEAT_CLEAVER" + ], + [ + "ITEM_TOOL_FORK_CARVING" + ], + [ + "ITEM_TOOL_NEST_BOX" + ], + [ + "ITEM_TOOL_JUG" + ], + [ + "ITEM_TOOL_LARGE_POT" + ], + [ + "ITEM_TOOL_HIVE" + ], + [ + "ITEM_TOOL_POUCH" + ], + [ + "ITEM_TOOL_WHEELBARROW" + ] + ], + "AMMO": [ + [ "ITEM_AMMO_BOLTS" - ], - "ITEM_WEAPON_LONGBOW": [ - "ITEM_AMMO_ARROWS_HUNTING", - "ITEM_AMMO_ARROWS_HBODKIN", - "ITEM_AMMO_ARROWS_WAR", + ], + [ + "ITEM_AMMO_ARROWS_HUNTING" + ], + [ + "ITEM_AMMO_ARROWS_HBODKIN" + ], + [ + "ITEM_AMMO_ARROWS_WAR" + ], + [ "ITEM_AMMO_ARROWS_WBODKIN" - ], - "ITEM_WEAPON_ATLATL": [ + ], + [ "ITEM_AMMO_DARTS_LONG" ] - }, + ], "PANTS": [ - "ITEM_PANTS_GREAVES_UPPER", - "ITEM_PANTS_PANTS", - "ITEM_PANTS_CHAUSSE", - "ITEM_PANTS_BRACCAE", - "ITEM_PANTS_SKIRT_LONG", - "ITEM_PANTS_LOINCLOTH" + [ + "ITEM_PANTS_GREAVES_UPPER", + "COMMON" + ], + [ + "ITEM_PANTS_PANTS", + "COMMON" + ], + [ + "ITEM_PANTS_CHAUSSE", + "COMMON" + ], + [ + "ITEM_PANTS_BRACCAE", + "COMMON" + ], + [ + "ITEM_PANTS_SKIRT_LONG", + "COMMON" + ], + [ + "ITEM_PANTS_LOINCLOTH", + "FORCED" + ] ] - }, + }, "FOREST": { "SHOES": [ - "ITEM_SHOES_CHAUSSE_CHAIN", - "ITEM_SHOES_GREAVES_LOWER", - "ITEM_SHOES_BOOTS", - "ITEM_SHOES_SANDALS", - "ITEM_SHOES_SOCKS" - ], + [ + "ITEM_SHOES_CHAUSSE_CHAIN", + "COMMON" + ], + [ + "ITEM_SHOES_GREAVES_LOWER", + "COMMON" + ], + [ + "ITEM_SHOES_BOOTS", + "COMMON" + ], + [ + "ITEM_SHOES_SANDALS", + "COMMON" + ], + [ + "ITEM_SHOES_SOCKS", + "FORCED" + ] + ], "ARMOR": [ - "ITEM_ARMOR_BYRNIE", - "ITEM_ARMOR_SCALE", - "ITEM_ARMOR_LAMELLAR", - "ITEM_ARMOR_CLOAK", - "ITEM_ARMOR_CAPE", - "ITEM_ARMOR_COAT", - "ITEM_ARMOR_VEST", - "ITEM_ARMOR_DRESS", - "ITEM_ARMOR_ROBE" - ], + [ + "ITEM_ARMOR_BYRNIE", + "COMMON" + ], + [ + "ITEM_ARMOR_SCALE", + "COMMON" + ], + [ + "ITEM_ARMOR_LAMELLAR", + "COMMON" + ], + [ + "ITEM_ARMOR_CLOAK", + "COMMON" + ], + [ + "ITEM_ARMOR_CAPE", + "COMMON" + ], + [ + "ITEM_ARMOR_COAT", + "COMMON" + ], + [ + "ITEM_ARMOR_VEST", + "COMMON" + ], + [ + "ITEM_ARMOR_DRESS", + "COMMON" + ], + [ + "ITEM_ARMOR_ROBE", + "FORCED" + ] + ], "WEAPON": [ - "ITEM_WEAPON_SWORD_SABER", - "ITEM_WEAPON_SWORD_SCIMITAR", - "ITEM_WEAPON_SWORD_FALCHION", - "ITEM_WEAPON_DAGGER_FIGHTING", - "ITEM_WEAPON_AXE_BARDICHE", - "ITEM_WEAPON_MACE_FLAIL_2H", - "ITEM_WEAPON_PIKE_GLAIVE", - "ITEM_WEAPON_PIKE_SWORD", - "ITEM_WEAPON_SPEAR", - "ITEM_WEAPON_SPEAR_SPETUM", - "ITEM_WEAPON_ATLATL", - "ITEM_WEAPON_BOW", - "ITEM_WEAPON_SWORD_TRAINING", - "ITEM_WEAPON_SPEAR_TRAINING", - "ITEM_WEAPON_MACE_TRAINING", - "ITEM_WEAPON_PIKE_TRAINING" - ], + [ + "ITEM_WEAPON_SWORD_SABER" + ], + [ + "ITEM_WEAPON_SWORD_SCIMITAR" + ], + [ + "ITEM_WEAPON_SWORD_FALCHION" + ], + [ + "ITEM_WEAPON_DAGGER_FIGHTING" + ], + [ + "ITEM_WEAPON_AXE_BARDICHE" + ], + [ + "ITEM_WEAPON_MACE_FLAIL_2H" + ], + [ + "ITEM_WEAPON_PIKE_GLAIVE" + ], + [ + "ITEM_WEAPON_PIKE_SWORD" + ], + [ + "ITEM_WEAPON_SPEAR" + ], + [ + "ITEM_WEAPON_SPEAR_SPETUM" + ], + [ + "ITEM_WEAPON_ATLATL" + ], + [ + "ITEM_WEAPON_BOW" + ], + [ + "ITEM_WEAPON_SWORD_TRAINING" + ], + [ + "ITEM_WEAPON_SPEAR_TRAINING" + ], + [ + "ITEM_WEAPON_MACE_TRAINING" + ], + [ + "ITEM_WEAPON_PIKE_TRAINING" + ] + ], "SHIELD": [ - "ITEM_SHIELD_SHIELD", - "ITEM_SHIELD_BUCKLER" - ], + [ + "ITEM_SHIELD_SHIELD" + ], + [ + "ITEM_SHIELD_BUCKLER" + ] + ], "GLOVES": [ - "ITEM_GLOVES_MITTENS", - "ITEM_GLOVES_GAUNTLETS", - "ITEM_GLOVES_GLOVES", - "ITEM_GLOVES_MITTENS_CHAIN", - "ITEM_GLOVES_GLOVES_CHAIN", - "ITEM_GLOVES_WRAP" - ], + [ + "ITEM_GLOVES_MITTENS", + "COMMON" + ], + [ + "ITEM_GLOVES_GAUNTLETS", + "COMMON" + ], + [ + "ITEM_GLOVES_GLOVES", + "COMMON" + ], + [ + "ITEM_GLOVES_MITTENS_CHAIN", + "COMMON" + ], + [ + "ITEM_GLOVES_GLOVES_CHAIN", + "COMMON" + ], + [ + "ITEM_GLOVES_WRAP", + "FORCED" + ] + ], "HELM": [ - "ITEM_HELM_NASAL", - "ITEM_HELM_BARBUTE", - "ITEM_HELM_SALLET", - "ITEM_HELM_COIF", - "ITEM_HELM_SKULLCAP", - "ITEM_HELM_HOOD", - "ITEM_HELM_SCARF_HEAD", - "ITEM_HELM_WRAP", - "ITEM_HELM_BANDANA" - ], - "AMMO": { - "ITEM_WEAPON_BOW": [ - "ITEM_AMMO_ARROWS_HUNTING", - "ITEM_AMMO_ARROWS_HBARB", - "ITEM_AMMO_ARROWS_WAR", - "ITEM_AMMO_ARROWS_WBARB" - ], - "ITEM_WEAPON_ATLATL": [ - "ITEM_AMMO_DARTS", + [ + "ITEM_HELM_NASAL", + "COMMON" + ], + [ + "ITEM_HELM_BARBUTE", + "COMMON" + ], + [ + "ITEM_HELM_SALLET", + "COMMON" + ], + [ + "ITEM_HELM_COIF", + "COMMON" + ], + [ + "ITEM_HELM_SKULLCAP", + "COMMON" + ], + [ + "ITEM_HELM_HOOD", + "COMMON" + ], + [ + "ITEM_HELM_SCARF_HEAD", + "COMMON" + ], + [ + "ITEM_HELM_WRAP", + "FORCED" + ], + [ + "ITEM_HELM_BANDANA", + "COMMON" + ] + ], + "AMMO": [ + [ + "ITEM_AMMO_DARTS" + ], + [ "ITEM_AMMO_DARTS_LONG" + ], + [ + "ITEM_AMMO_ARROWS_HUNTING" + ], + [ + "ITEM_AMMO_ARROWS_HBARB" + ], + [ + "ITEM_AMMO_ARROWS_WAR" + ], + [ + "ITEM_AMMO_ARROWS_WBARB" ] - }, + ], "PANTS": [ - "ITEM_PANTS_BRACCAE", - "ITEM_PANTS_GREAVES_UPPER", - "ITEM_PANTS_PANTS", - "ITEM_PANTS_CHAUSSE", - "ITEM_PANTS_CHAUSSE_CHAIN", - "ITEM_PANTS_SKIRT_SHORT", - "ITEM_PANTS_SKIRT", - "ITEM_PANTS_SKIRT_LONG", - "ITEM_PANTS_LOINCLOTH" + [ + "ITEM_PANTS_BRACCAE", + "COMMON" + ], + [ + "ITEM_PANTS_GREAVES_UPPER", + "COMMON" + ], + [ + "ITEM_PANTS_PANTS", + "COMMON" + ], + [ + "ITEM_PANTS_CHAUSSE", + "COMMON" + ], + [ + "ITEM_PANTS_CHAUSSE_CHAIN", + "COMMON" + ], + [ + "ITEM_PANTS_SKIRT_SHORT", + "COMMON" + ], + [ + "ITEM_PANTS_SKIRT", + "COMMON" + ], + [ + "ITEM_PANTS_SKIRT_LONG", + "COMMON" + ], + [ + "ITEM_PANTS_LOINCLOTH", + "FORCED" + ] ] } -} +} \ No newline at end of file diff --git a/scripts/stal/armouryentities.py b/scripts/stal/armouryentities.py index 4818dd1..ea068ad 100644 --- a/scripts/stal/armouryentities.py +++ b/scripts/stal/armouryentities.py @@ -6,29 +6,24 @@ import json import raws -print 'And so it begins.' +print('And so it begins.') -entities = raws.dir(path='StalsArmouryPackv1_8a_4024')['entity_default'] +armouryraws = raws.dir(root='raw/armoury') + +itemtypes = ('AMMO', 'DIGGER', 'TOOL', 'WEAPON', 'ARMOR', 'PANTS', 'GLOVES', 'SHOES', 'HELM', 'SHIELD') edict = {} -for entity in entities.all(exact_value='ENTITY'): - print 'Entity: %s' % entity - itemtypes = ('AMMO', 'DIGGER', 'TOOL', 'WEAPON', 'ARMOR', 'PANTS', 'GLOVES', 'SHOES', 'HELM', 'SHIELD') +for entity in armouryraws.allobj('ENTITY'): + print('Entity: %s' % entity) edict[entity.args[0]] = {} entitydict = edict[entity.args[0]] - for item in entity.alluntil(value_in=itemtypes, until_exact_value='ENTITY'): - if item.value == 'AMMO': - if item.value not in entitydict: entitydict[item.value] = {} - forweapon = item.get(exact_value='WEAPON', reverse=True).args[0] - if forweapon not in entitydict[item.value]: entitydict[item.value][forweapon] = [] - entitydict[item.value][forweapon].append(item.args[0]) - else: - if item.value not in entitydict: entitydict[item.value] = [] - entitydict[item.value].append(item.args[0]) + for item in entity.allprop(value_in=itemtypes): + if item.value not in entitydict: entitydict[item.value] = [] + entitydict[item.value].append(item.args) -print edict +print(edict) -with open('armouryentities.json', 'wb') as efile: json.dump(edict, efile) +with open('armouryentities.json', 'wb') as efile: json.dump(edict, efile, indent=4) -print 'All done!' +print('All done!') diff --git a/scripts/stal/StalsArmouryPackv1_8a_4024/readme_armoury.txt b/scripts/stal/misc/readme_armoury.txt similarity index 100% rename from scripts/stal/StalsArmouryPackv1_8a_4024/readme_armoury.txt rename to scripts/stal/misc/readme_armoury.txt diff --git a/scripts/stal/pydwarf.armoury.py b/scripts/stal/pydwarf.armoury.py new file mode 100644 index 0000000..e51fbbc --- /dev/null +++ b/scripts/stal/pydwarf.armoury.py @@ -0,0 +1,194 @@ +import os +import json +import pydwarf +import raws + +# Armoury raws are located in this directory +entities_json = pydwarf.rel(__file__, 'armouryentities.json') +armoury_dir = pydwarf.rel(__file__, 'raw/armoury') + +# Armory raws are variations on vanilla names, this records them so they can be changed back. +# Without this, other mods which attempt to make changes to the same items might run into problems. +weird_armoury_names = { + 'ITEM_AMMO_ARROWS_WAR': 'ITEM_AMMO_ARROWS', + 'ITEM_ARMOR_JERKIN': 'ITEM_ARMOR_LEATHER', + 'ITEM_ARMOR_PLATE': 'ITEM_ARMOR_BREASTPLATE', + 'ITEM_SHOES_SANDALS': 'ITEM_SHOES_SANDAL', + 'ITEM_WEAPON_MACE_MORNINGSTAR': 'ITEM_WEAPON_MORNINGSTAR', + 'ITEM_WEAPON_HAMMER_MAUL': 'ITEM_WEAPON_MAUL', + 'ITEM_WEAPON_MACE_FLAIL': 'ITEM_WEAPON_FLAIL', + 'ITEM_WEAPON_PIKE_HALBERD': 'ITEM_WEAPON_HALBERD', + 'ITEM_WEAPON_PIKE_PIKE': 'ITEM_WEAPON_PIKE', + 'ITEM_WEAPON_SWORD_SCIMITAR': 'ITEM_WEAPON_SCIMITAR', + 'ITEM_WEAPON_SWORD_TRAINING': 'ITEM_WEAPON_SWORD_SHORT_TRAINING', + 'ITEM_WEAPON_WHIP_SCOURGE': 'ITEM_WEAPON_SCOURGE' +} + +# These are vanilla items without obvious analogs in the armoury raws. +# Mostly just here for my own sake. +missing_armoury_names = [ + 'ITEM_ARMOR_MAIL_SHIRT', + 'ITEM_ARMOR_TOGA', + 'ITEM_HELM_HELM', + 'ITEM_HELM_MASK', + 'ITEM_HELM_TURBAN', + 'ITEM_HELM_VEIL_FACE', + 'ITEM_HELM_VEIL_HEAD', + 'ITEM_SHOES_BOOTS_LOW', + 'ITEM_SHOES_CHAUSSE', + 'ITEM_WEAPON_AXE_GREAT', + 'ITEM_WEAPON_DAGGER_LARGE', + 'ITEM_WEAPON_SWORD_LONG', + 'ITEM_WEAPON_SWORD_SHORT' +] + +# These are the only item types we care about. +armoury_items = [ + 'AMMO', + 'ARMOR', + 'HELM', + 'GLOVES', + 'PANTS', + 'SHIELD', + 'SHOES', + 'TOOL', + 'WEAPON', +] +armoury_item_objects = ['ITEM_%s' % item for item in armoury_items] + +# These are the items that each entity should have. (JSON courtesy of helper script armouryentities.py.) +with open(entities_json, 'rb') as efile: armoury_entities = json.load(efile) + + + +@pydwarf.urist( + name = 'stal.armoury.items', + version = '1.0.0', + author = ('Stalhansch', 'Sophie Kirschner'), + description = 'Attempts to improve the balance and realism of combat.', + arguments = { + 'remove_entity_items': '''Determines whether items that would be made unavailable to + entities should be removed from those entities or not. If you're also using other + mods that make changes to weapons and armour and such it may be desireable to set + this flag to False. Otherwise, for best results, the flag should be set to True. + Defaults to True''' + }, + compatibility = pydwarf.df_revision_range('0.40.14', '0.40.24') +) +def armoury(df, remove_entity_items=True): + try: + pydwarf.log.debug('Loading armoury raws from %s.' % armoury_dir) + armouryraws = raws.dir(root=armoury_dir, log=pydwarf.log) + except: + return pydwarf.failure('Unable to load armoury raws.') + + # Persist a list of armoury item tokens because we're going to be needing them for a few things + armouryitemtokens = armouryraws.allobj(type_in=armoury_item_objects) + + # Rename items in the armoury raws in accordance with a manually compiled list of naming oddities + for item in armouryitemtokens.all(arg_in=(0, weird_armoury_names)): + item.args[0] = weird_armoury_names[item.args[0]] + + # Remove any existing items in the raws which share an id with the items about to be added + pydwarf.log.debug('Removing obsolete items.') + df.removeallobj(type_in=armoury_item_objects, id_in=[token.arg() for token in armouryitemtokens]) + + # Look for files made empty as a result (of which there should be a few) and remove them + removedfiles = [] + for file in df.files.values(): + if isinstance(file, raws.rawfile) and len(file) <= 1: + pydwarf.log.debug('Removing emptied file %s.' % file) + file.remove() + removedfiles.append(file) + + # Get a list of entity tokens corresponding to the relevant names + entitytokens = [df.getobj('ENTITY', entity) for entity in armoury_entities] + + # If remove_entity_items is set to True, remove all existing tokens which permit relevant items + # Otherwise, just remove the ones with conflict with those about to be added + for entitytoken in entitytokens: + pydwarf.log.debug('Removing permission tokens from %s.' % entitytoken) + entitytoken.removeallprop( + value_in = armoury_items, + arg_in = ( + None if remove_entity_items else (0, [token.args[0] for token in armouryitemtokens]) + ) + ) + + # Now add all the armoury raw files that have items in them + try: + for file in armouryraws.iterfiles(): + if file.kind == 'raw' and file.name.startswith('item_'): + pydwarf.log.debug('Adding file %s to raws.' % file) + copy = file.copy() + copy.loc = 'raw/objects' + copy.name += '_armoury_stal' + df.add(copy) + except: + pydwarf.log.exception('Encountered exception.') + return pydwarf.failure('Failed to add armoury item raws.') + + # Add new permitted item tokens + try: + for entitytoken in entitytokens: + pydwarf.log.debug('Permitting items for %s.' % entitytoken) + for itemtype, items in armoury_entities[entitytoken.arg()].iteritems(): + for item in items: + entitytoken.add( + raws.token( + value = itemtype, + args = [weird_armoury_names.get(item[0], item[0])] + item[1:] + ) + ) + except: + pydwarf.log.exception('Encountered exception.') + return pydwarf.failure('Failed to permit items for entities.') + + # And add the new reactions + try: + pydwarf.log.debug('Adding new reactions.') + reactions = armouryraws['reaction_armoury'].copy() + reactions.loc = 'raw/objects' + reactions.name = 'reaction_armoury_stal' + response = pydwarf.urist.getfn('pineapple.easypatch')( + df, + reactions, + permit_entities = armoury_entities.keys() + ) + if not response: return response + except: + pydwarf.log.exception('Encountered exception.') + return pydwarf.failure('Failed to add new reactions.') + + # All done! + return pydwarf.success() + + + +@pydwarf.urist( + name = 'stal.armoury.attacks', + version = '1.0.0', + author = ('Stalhansch', 'Sophie Kirschner'), + description = '''Removes attacks from creatures. By default, as a way to improve balance in + combat, scratch and bite attacks are removed from dwarves, humans, and elves.''', + arguments = { + 'remove_attacks': '''Removes these attacks from species listed in remove_attacks_from. + Defaults to scratch and bite.''', + 'remove_attacks_from': '''If set to True, specified remove_attacks are removed + from the species in the list to improve combat balancing. If set to None those + attacks will not be touched. Defaults to dwarves, humans, and elves.''' + }, + compatibility = (pydwarf.df_0_40, pydwarf.df_0_3x) +) +def removeattacks(df, remove_attacks=('SCRATCH', 'BITE'), remove_attacks_from=('DWARF', 'HUMAN', 'ELF')): + removed = 0 + for creature in df.allobj(type='CREATURE', id_in=remove_attacks_from): + for attack in creature.allprop(exact_value='ATTACK', arg_in=((0, remove_attacks),)): + pydwarf.log.debug('Removing attack %s from creature %s.' % (attack, creature)) + for token in attack.alluntil(until_re_value='(?!ATTACK_).+'): token.remove() + attack.remove() + removed += 1 + if removed: + return pydwarf.success('Removed %d attacks from %d creatures.' % (removed, len(remove_attacks_from))) + else: + return pydwarf.failure('Removed no attacks from creatures.') diff --git a/scripts/stal/pydwarf.armourypack.py b/scripts/stal/pydwarf.armourypack.py deleted file mode 100644 index 5439ff1..0000000 --- a/scripts/stal/pydwarf.armourypack.py +++ /dev/null @@ -1,201 +0,0 @@ -import os -import json -import pydwarf -import raws - -# Armoury raws are located in this directory -armourydir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'StalsArmouryPackv1_8a_4024') - -# Armory raws are variations on vanilla names, this records them so they can be changed back. -# Without this, other mods which attempt to make changes to the same items might run into problems. -weird_armoury_names = { - 'ITEM_AMMO_ARROWS_WAR': 'ITEM_AMMO_ARROWS', - 'ITEM_ARMOR_JERKIN': 'ITEM_ARMOR_LEATHER', - 'ITEM_ARMOR_PLATE': 'ITEM_ARMOR_BREASTPLATE', - 'ITEM_SHOES_SANDALS': 'ITEM_SHOES_SANDAL', - 'ITEM_WEAPON_MACE_MORNINGSTAR': 'ITEM_WEAPON_MORNINGSTAR', - 'ITEM_WEAPON_HAMMER_MAUL': 'ITEM_WEAPON_MAUL', - 'ITEM_WEAPON_MACE_FLAIL': 'ITEM_WEAPON_FLAIL', - 'ITEM_WEAPON_PIKE_HALBERD': 'ITEM_WEAPON_HALBERD', - 'ITEM_WEAPON_PIKE_PIKE': 'ITEM_WEAPON_PIKE', - 'ITEM_WEAPON_SWORD_SCIMITAR': 'ITEM_WEAPON_SCIMITAR', - 'ITEM_WEAPON_SWORD_TRAINING': 'ITEM_WEAPON_SWORD_SHORT_TRAINING', - 'ITEM_WEAPON_WHIP_SCOURGE': 'ITEM_WEAPON_SCOURGE' -} - -# These are vanilla items without obvious analogs in the armoury raws. -# Mostly just here for my own sake. -missing_armoury_names = [ - 'ITEM_ARMOR_MAIL_SHIRT', - 'ITEM_ARMOR_TOGA', - 'ITEM_HELM_HELM', - 'ITEM_HELM_MASK', - 'ITEM_HELM_TURBAN', - 'ITEM_HELM_VEIL_FACE', - 'ITEM_HELM_VEIL_HEAD', - 'ITEM_SHOES_BOOTS_LOW', - 'ITEM_SHOES_CHAUSSE', - 'ITEM_WEAPON_AXE_GREAT', - 'ITEM_WEAPON_DAGGER_LARGE', - 'ITEM_WEAPON_SWORD_LONG', - 'ITEM_WEAPON_SWORD_SHORT' -] - -# These are the only item types we care about. -armoury_items = [ - 'ITEM_AMMO', - 'ITEM_ARMOR', - 'ITEM_HELM', - 'ITEM_GLOVES' - 'ITEM_PANTS', - 'ITEM_SHIELD', - 'ITEM_SHOES', - 'ITEM_TOOL', - 'ITEM_WEAPON', -] - -# These are the items that each entity should have. (JSON courtesy of helper script armouryentities.py.) -with open(os.path.join(armourydir, '../armouryentities.json'), 'rb') as efile: armoury_entities = json.load(efile) - - - -def additemstoraws(dfraws, armouryraws): - pydwarf.log.debug('Building dict to index relevant items in df raws...') - dfitems = {str(token): token for token in dfraws.all(value_in=armoury_items)} - pydwarf.log.debug('Dict dfitems contains %d tokens.' % len(dfitems)) - - # Add new items to raws and edit already-present ones - for filename in armouryraws.files.keys(): - if filename.startswith('item_'): - for armouryitem in armouryraws[filename].all(value_in=armoury_items, args_count=1): - pydwarf.log.debug('Handling armoury item %s...' % armouryitem) - - # Account for names that were changed from the normal DF raws - if armouryitem.args[0] in weird_armoury_names: armouryitem.args[0] = weird_armoury_names[armouryitem.args[0]] - - # Get the tokens belonging to this item - armourytokens = armouryitem.alluntil(until_exact_value=armouryitem.value) - # Get the current item with the same ID (if present) - dfitem = dfitems.get(str(armouryitem)) - - # Replace item properties - if dfitem and armouryitem: - pydwarf.log.debug('Replacing properties of item %s...' % armouryitem) - for removeitem in dfitem.alluntil(until_re_value='ITEM_.+'): removeitem.remove() - dfitem.add(tokens=raws.token.copy(armourytokens)) - del dfitems[str(armouryitem)] - - # Add new item - elif armourytokens: - pydwarf.log.debug('Adding new item %s...' % armouryitem) - if filename not in dfraws.files: dfraws.addfile(filename) - armouryitem.prefix = '\n\n' # Makes outputted raws a bit neater - dfraws[filename].add(token=armouryitem) - dfraws[filename].add(tokens=raws.token.copy(armourytokens)) - - # This really shouldn't happen, but check for it anyway - else: - pydwarf.log.error('Found no tokens belonging to armoury item %s.' % armouryitem) - -def additemstoents(dfraws, armouryraws, remove_entity_items): - # Screw around with which items are allowed for which entities - for entityname, aentity in armoury_entities.iteritems(): - dfentity = dfraws.get(exact_value='ENTITY', exact_args=[entityname]) - entityitems = {} - if dfentity: - pydwarf.log.debug('Handling entity %s...' % dfentity) - - # Maintain this dict because it makes ammo easier to add - weapons = {} - - # If we're removing items, just remove all the present ones in one go before adding things back - if remove_entity_items: - for itemtoken in dfentity.alluntil(value_in=armoury_entities, args_count=1, until_exact_value='ENTITY'): itemtoken.remove() - - # Time to add the items! - for itemtype, items in aentity.iteritems(): - if itemtype != 'AMMO': - for itemname in items: - # Account for names that were changed from the normal DF raws - if itemname in weird_armoury_names: itemname = weird_armoury_names[itemname] - # Add the token if it isn't there already - dftoken = None if remove_entity_items else dfentity.getuntil(exact_value=itemtype, exact_args=[itemname], until_exact_value='ENTITY') - if remove_entity_items or not dftoken: dftoken = dfentity.add(raws.token(value=itemtype, args=[itemname])) - if itemtype == 'WEAPON': weapons[itemname] = dftoken - - # Now add the ammunition - if 'AMMO' in aentity: - for weaponname, ammos in aentity['AMMO'].iteritems(): - weapontoken = weapons.get(weaponname) - if not weapontoken: weapontoken = dfentity.get(exact_value='WEAPON', exact_args=[weaponname]) - if weapontoken: - ammotokens = {token.args[0]: token for token in weapontoken.alluntil(exact_value='AMMO', args_count=1, until_except_value='AMMO')} - for addammo in ammos: - if addammo not in ammotokens: weapontoken.add(raws.token(value='AMMO', args=[addammo])) - else: - pydwarf.log.error('Failed to add ammo %s to weapon %s.' % ammos, weaponname) - - else: - pydwarf.log.error('Failed to find entity %s for editing.' % entityname) - -def addreactions(dfraws, armouryraws): - # Add raws file containing new reactions - armouryreactions = armouryraws['reaction_armoury'] - if armouryreactions: - if 'stal_reaction_armoury' not in dfraws.files: - armouryreactions.header = 'stal_reaction_armoury' - dfraws.files['stal_reaction_armoury'] = armouryreactions - else: - pydwarf.log.error('DF raws already contain stal_reaction_armory.') - else: - pydwarf.log.error('Couldn\'t load reaction_armoury raws file for reading.') - -def removeattacks(dfraws, remove_attacks, remove_attacks_from): - # Removes e.g. bite and scratch attacks from e.g. dwarves, humans, and elves - if remove_attacks is not None and remove_attacks_from is not None: - for species in remove_attacks_from: - creaturetoken = dfraws.get(exact_value='CREATURE', exact_args=[species]) - if creaturetoken: - for attacktype in remove_attacks: - attacktokens = creaturetoken.alluntil(exact_value='ATTACK', exact_arg=((0, attacktype),), until_exact_value='CREATURE') - for attacktoken in attacktokens: - subtokens = attacktoken.alluntil(until_re_value='(?!ATTACK_).+') - for subtoken in subtokens: subtoken.remove() - attacktoken.remove() - pydwarf.log.debug('Removed attack %s from creature %s.' % (attacktype, species)) - else: - pydwarf.log.error('Couldn\'t find creature %s to remove bite and scratch attacks from.' % species) - - - -@pydwarf.urist( - name = 'stal.armoury', - version = '1.0.0', - author = ('Stalhansch', 'Sophie Kirschner'), - description = 'Attempts to improve the balance and realism of combat.', - arguments = { - 'remove_entity_items': '''Determines whether items that would be made unavailable to - entities should be removed from those entities or not. If you're also using other - mods that make changes to weapons and armour and such it may be desireable to set - this flag to False. Otherwise, for best results, the flag should be set to True. - Defaults to True''', - 'remove_attacks': '''Removes these attacks from species listed in remove_attacks_from. - Defaults to scratch and bite.''', - 'remove_attacks_from': '''If set to True, specified remove_attacks are removed - from the species in the list to improve combat balancing. If set to None those - attacks will not be touched. Defaults to dwarves, humans, and elves.''' - }, - compatibility = pydwarf.df_revision_range('0.40.14', '0.40.24') -) -def armourypack(dfraws, remove_entity_items=True, remove_attacks=('SCRATCH', 'BITE'), remove_attacks_from=('DWARF', 'HUMAN', 'ELF')): - try: - armouryraws = raws.dir(path=armourydir, log=pydwarf.log) - except: - return pydwarf.failure('Unable to load armoury raws.') - - additemstoraws(dfraws, armouryraws) - additemstoents(dfraws, armouryraws, remove_entity_items) - addreactions(dfraws, armouryraws) - removeattacks(dfraws, remove_attacks, remove_attacks_from) - - return pydwarf.success() diff --git a/scripts/stal/StalsArmouryPackv1_8a_4024/creature_standard.txt b/scripts/stal/raw/armoury/creature_standard.txt similarity index 100% rename from scripts/stal/StalsArmouryPackv1_8a_4024/creature_standard.txt rename to scripts/stal/raw/armoury/creature_standard.txt diff --git a/scripts/stal/StalsArmouryPackv1_8a_4024/entity_default.txt b/scripts/stal/raw/armoury/entity_default.txt similarity index 100% rename from scripts/stal/StalsArmouryPackv1_8a_4024/entity_default.txt rename to scripts/stal/raw/armoury/entity_default.txt diff --git a/scripts/stal/StalsArmouryPackv1_8a_4024/item_ammo.txt b/scripts/stal/raw/armoury/item_ammo.txt similarity index 100% rename from scripts/stal/StalsArmouryPackv1_8a_4024/item_ammo.txt rename to scripts/stal/raw/armoury/item_ammo.txt diff --git a/scripts/stal/StalsArmouryPackv1_8a_4024/item_armor.txt b/scripts/stal/raw/armoury/item_armor.txt similarity index 100% rename from scripts/stal/StalsArmouryPackv1_8a_4024/item_armor.txt rename to scripts/stal/raw/armoury/item_armor.txt diff --git a/scripts/stal/StalsArmouryPackv1_8a_4024/item_body.txt b/scripts/stal/raw/armoury/item_body.txt similarity index 100% rename from scripts/stal/StalsArmouryPackv1_8a_4024/item_body.txt rename to scripts/stal/raw/armoury/item_body.txt diff --git a/scripts/stal/StalsArmouryPackv1_8a_4024/item_gloves.txt b/scripts/stal/raw/armoury/item_gloves.txt similarity index 100% rename from scripts/stal/StalsArmouryPackv1_8a_4024/item_gloves.txt rename to scripts/stal/raw/armoury/item_gloves.txt diff --git a/scripts/stal/StalsArmouryPackv1_8a_4024/item_helm.txt b/scripts/stal/raw/armoury/item_helm.txt similarity index 100% rename from scripts/stal/StalsArmouryPackv1_8a_4024/item_helm.txt rename to scripts/stal/raw/armoury/item_helm.txt diff --git a/scripts/stal/StalsArmouryPackv1_8a_4024/item_pants.txt b/scripts/stal/raw/armoury/item_pants.txt similarity index 100% rename from scripts/stal/StalsArmouryPackv1_8a_4024/item_pants.txt rename to scripts/stal/raw/armoury/item_pants.txt diff --git a/scripts/stal/StalsArmouryPackv1_8a_4024/item_shield.txt b/scripts/stal/raw/armoury/item_shield.txt similarity index 100% rename from scripts/stal/StalsArmouryPackv1_8a_4024/item_shield.txt rename to scripts/stal/raw/armoury/item_shield.txt diff --git a/scripts/stal/StalsArmouryPackv1_8a_4024/item_shoes.txt b/scripts/stal/raw/armoury/item_shoes.txt similarity index 100% rename from scripts/stal/StalsArmouryPackv1_8a_4024/item_shoes.txt rename to scripts/stal/raw/armoury/item_shoes.txt diff --git a/scripts/stal/StalsArmouryPackv1_8a_4024/item_tool.txt b/scripts/stal/raw/armoury/item_tool.txt similarity index 100% rename from scripts/stal/StalsArmouryPackv1_8a_4024/item_tool.txt rename to scripts/stal/raw/armoury/item_tool.txt diff --git a/scripts/stal/StalsArmouryPackv1_8a_4024/item_weapon.txt b/scripts/stal/raw/armoury/item_weapon.txt similarity index 100% rename from scripts/stal/StalsArmouryPackv1_8a_4024/item_weapon.txt rename to scripts/stal/raw/armoury/item_weapon.txt diff --git a/scripts/stal/StalsArmouryPackv1_8a_4024/reaction_armoury.txt b/scripts/stal/raw/armoury/reaction_armoury.txt similarity index 100% rename from scripts/stal/StalsArmouryPackv1_8a_4024/reaction_armoury.txt rename to scripts/stal/raw/armoury/reaction_armoury.txt diff --git a/scripts/stal/readme.md b/scripts/stal/readme.md new file mode 100644 index 0000000..18abdd6 --- /dev/null +++ b/scripts/stal/readme.md @@ -0,0 +1,72 @@ +# Stal's Armoury Pack + +Created by Stalhansch. (www.bay12forums.com/smf/index.php?topic=142993.0) +Rewritten for PyDwarf by Sophie Kirschner. (http://www.pineapplemachine.com) + +## Introduction + +A couple of years ago I became rather obsessed with understanding the Dwarf Fortress combat system. I was often annoyed by how armour didn't have as much of an impact in battle as it should. Swords and axes were prone to severing limbs straight through and spears easily pierced the entirety of legs, even when made of the same material as the armour they were seemingly ignoring. I was further bothered by ranged being the end-all of combat more often than not. + +This led me to many a night of searches and inspections of raws from several different mods, trying to figure out how they attempted to solved this perennial problem of mine. I ultimately stumbled across Lucelle's mod. After playing around with Lucelle's mod, I was inspired to try and fix the problem myself. + +This small project is the result of almost 2 years of tweaking and too many hours spent testing in the Arena. I think it's finally come to a point where it is as balanced and sensical as it's going to be with only myself to scrutinise (One of my primary goals was to have weapons behave as they would in real life, but this is Dwarf Fortress and there will be anomalies). Of course, my idea of balance is only my own, and this is what has brought me to release this project to the public. I invite you to either help me improve this mod with critique and suggestions or simply enjoy it any other way you see fit. + +## Scripts + +### stal.armoury.attacks + +Removes the ridiculously effective bite and scratch attacks from humans, dwarves and elves. + +### stal.armoury.items + +Adds new items to make combat more balanced and varied. + +#### Weapons + +**Diggers:** Pickaxe, Mattock. + +**Daggers:** Short seax, Rondel dagger, Fighting knife. + +**One-handed swords:** Long seax, Narrow seax, Broad seax, Short arming sword, Arming sword, Long arming sword, Sea-raider sword, Saber, Scimitar, Falchion. + +**Two-handed swords:** Bastard sword, Great sword, Two-handed sword. + +**One-handed axes:** Hatchet, Adze, Battle axe, Sea-raider axe, Axehammer. + +**Two-handed axes:** Hafted sea-raider axe, Bardiche. + +**One-handed maces:** Round mace, Knobbed mace, Flanged mace, Pernach, Morningstar, Flail, Spiked flail. + +**Two-handed maces:** Hafted flail, Goedendag. + +**One-handed hammers:** War hammer, War pick. + +**Two-handed hammers:** Hafted war hammer, Maul. + +**Lashes:** Whip, Hooked whip, Knotted whip, Scourge. + +**Spears:** Spear, Boar spear, Partisan, Hewing spear. + +**Stabbing pikes:** Awl pike, Pike, Guisarme. + +**Slashing pikes:** Glaive, Sword staff, Halberd, Voulge. + +**Ranged:** Crossbow, Bayoneted crossbow, Siege crossbow, Recurve bow, Longbow, Blowgun, Atlatl, Amentum, Sling, Staff sling. + +**Ammo:** Bolt, Barbed bolt, Quarrel, Hunting arrow, Barbed hunting arrow, Bodkin hunting arrow, War arrow, Barbed war arrow, Bodkin war arrow, Dart, Long dart, Javelin, Bullet, Blowdart. + +**Training weapons:** Training axe, Training sword, Training spear, Training hammer, Training mace, Training pike. + +#### Attire + +**Armor:** Breastplate, Scale armor, Lamellar armor, Brigandine, Hauberk, Byrnie, Haubergeon, Armor, Gambeson, Aketon, Cloak, Surcoat, Tabard, Cape, Coat, Robe, Dress, Vest, Shirt, Tunic. + +**Helmets:** Great helm, Bascinet, Sea-raider helmet, Sallet, Barbute, Kettle helmet, Nasal helmet, Skull cap, Coif, Padded coif, Hood, Arming cap, Cap, Headscarf, Couvrechef, Head wrap. + +**Gloves:** Gauntlet, Mail glove, Mail mitten, Mitten, Glove, Hand wrap. + +**Shoes:** Lower greave, Lower chausse, Boot, Turnshoe, Carbatina, Ankle boot, Caliga, Leg wrap. + +**Pants:** Upper greaves, Upper chausses, Hose, Trousers, Braccae, Short skirt, Long skirt, Braies, Loincloth. + +**Shields:** Buckler, Round shield, Heater shield, Kite shield, Tower shield. diff --git a/scripts/stal/readme.txt b/scripts/stal/readme.txt deleted file mode 100644 index ba3ff37..0000000 --- a/scripts/stal/readme.txt +++ /dev/null @@ -1,15 +0,0 @@ -Armoury Pack v1.8 created by Stalhansch. (www.bay12forums.com/smf/index.php?topic=142993.0) -Armoury Pack v1.8 ported to PyDwarf by Sophie Kirschner. (http://www.pineapplemachine.com) - -A couple of years ago I became rather obsessed with understanding the Dwarf Fortress combat system. I was often annoyed by how armour didn't have as much of an impact in battle as it should. Swords and axes were prone to severing limbs straight through and spears easily pierced the entirety of legs, even when made of the same material as the armour they were seemingly ignoring. I was further bothered by ranged being the end-all of combat more often than not. - -This led me to many a night of searches and inspections of raws from several different mods, trying to figure out how they attempted to solved this perennial problem of mine. I ultimately stumbled across Lucelle's mod. After playing around with Lucelle's mod, I was inspired to try and fix the problem myself. - -This small project is the result of almost 2 years of tweaking and too many hours spent testing in the Arena. I think it's finally come to a point where it is as balanced and sensical as it's going to be with only myself to scrutinise (One of my primary goals was to have weapons behave as they would in real life, but this is Dwarf Fortress and there will be anomalies). Of course, my idea of balance is only my own, and this is what has brought me to release this project to the public. I invite you to either help me improve this mod with critique and suggestions or simply enjoy it any other way you see fit. - -v1.8 Changelog: -- Fixed some typos. -- Some weapons with only one attack option no longer specify with which part of the weapon the attack is executed. -- Added [CHAIN_METAL_TEXT] to some items of armour that were lacking it. -- Renamed civilian "chausses" to "hose" for better distinction between the cloth and armour variants. -- Renamed and tweaked both shoe-type chausses and pants-type chausses to fit in the same manner as greaves. diff --git a/scripts/umiman/data/smallthings/no_family.txt b/scripts/umiman/data/smallthings/no_family.txt new file mode 100644 index 0000000..51da486 --- /dev/null +++ b/scripts/umiman/data/smallthings/no_family.txt @@ -0,0 +1,39 @@ +I have no family to speak of +I have no family +I have no children, nor lover or mother or father +I am alone +Family is for the weak +I have no material ties to this world +Where I am from, it is considered rude for a stranger to ask such personal questions +Family is meaningless +I refuse to answer that question +Why do you ask me such things? +Family... would be nice +I could've had a family +That question is painful, don't ask it again +What would it matter to you? +I live alone +I have no family for now, but maybe in the future +I have no family now, but my heart is set on another. Ask me in the future when I succeed +... +Tell me about YOUR family +Stop asking me these things +I'm afraid I don't remember +Wh-why do you ask me if I have family? O-o-of course I have a family! Ha! Ha! Ha! What kind of silly fool wouldn't have a family? It must be quite silly to not have a family, wouldn't it? Hahahahaha!!! +That question is pointless +Leave me alone already +I don't have a family and I don't care for one either +I don't have a family. Let's just leave it at that +I have always only depended on myself +I only need strength. Love is but an illusion +Why don't you join me for some drinks and we'll discuss this further at my home +My friends are to me what family is to you +Why do you ask? Are you looking for love? +That's a complicated question with a complicated answer. Ask me something else +Stop asking all these senseless questions! +Don't you have something more important to do? +Family only gets in the way, wouldn't you agree? +My family is in a far away land +I came here to forget my past, please don't ask again +By the Gods, you are annoying +Family, huh? Family... family... family... \ No newline at end of file diff --git a/scripts/umiman/data/smallthings/threat.txt b/scripts/umiman/data/smallthings/threat.txt new file mode 100644 index 0000000..3e2e690 --- /dev/null +++ b/scripts/umiman/data/smallthings/threat.txt @@ -0,0 +1,184 @@ +Prepare to die! +Your blood shall be spilled this day! +I shall delight in your torture! +I shall feast on your entrails! +I damn you to oblivion! +I am going to split you like a ripe melon! +Pray while you still can! +Pray to your maker! +Submit to the cold embrace of death! +Your head will hang on my wall! +Give in to darkness and despair! +You shall burn in eternal hell! +Your life belongs to me! +I will spill your blood across the ground and dance upon your cooling corpse! +What noise will you make when I tear off your limbs one by one? +My god will torture your soul forever when I present him with your charred corpse! +You shall pay for your trespass with your life! +Your agony shall certainly please my god! +I will bet on how many organs I can remove before you stop making noise! +It has been long since I have seen one of your kind die! +After I tear off your limbs, you will squirm until I crush you in my endless mercy! +You will beg for death before I am done! +Die! Die! DIE! +You will be proud to die at my hands! +Perhaps you will be a worthy prey! +There is no escape! +Are you yet afraid? If not, you will be! +You will know nothing but terror when I am through! +I shall scatter your shredded corpse across the land for all to see! +Your body will fall like a limp bag! +Scream now, for you will soon be unable to speak! +Your screams of agony will give me orgastic dreams this night! +I will show you the true meaning of horror! +Have dread, your life ends now! +Yet another [AUDIENCE:RACE] shall kneel before me! +Your innards will soon fly! +Your flesh shall soon meet my weapon! +You will pay for this! +WHAAAAAGGGGHHHH! +Time to die, meat! +I will crush your brains to paste! +I shall exterminate you, vermin! +You will never stop me! +Feel my wrath! +The gods will find your soul unworthy! +Catch this! With your face! +I shall soon teach you in the ways of pain! +That's right! Run! +Your struggles will hardly trouble me! +You blighter! +I will not let this trespass stand! +Your eyes will burst like sickly boils! +You will soon wear your intestines around your neck! +Blood for the Blood God! +Prepare to be blown apart! +I shall draw first blood here! +I will smack you upside the head with a cave fish! +Your universe will become pain! +The stars have foreseen my victory over you, worm! +Soon your life shall be snuffed! +I shall knock you from the frozen wastes to the burning deserts! +You will be stomped into the ground! +I will bash you until your horrid face is unrecognizable! +Can you conceive the wrath you have called down? +You are making me angry, fool. You would not like me when I am angry. +I will wear you lungs as my vest! +I will knit your hair into a merkin! (look it up, but you might be sorry...) +Your suffering will be legendary! +I have been waiting for this! +Perhaps you would like a moment to change your trousers? Ha ha ha ha! +Do you hear that sound? It is the sound of your doom, growing closer, closer, closer! +I would ask your name, but you will be another anonymous corpse soon enough! +Now that we are introduced, let us fight! +You will not take me today! +I guess I must kill you now. So be it! +I will strike you so hard, your ancestors will feel it! +You should just kill yourself now to save us both time, you know... +I will reduce you to a pile of gore! +Stand and fight! +Show me what you've got! +You should've stayed home! +Death is the only escape. +Life is meaningless. It is in death that we are truly tested. +You seek death? +Be still! +You will find no peace in death. +One foot in the grave, firmly planted. +Sheathe your sword, warrior, lest ye fall on it when stricken +What was yours is mine. Your land, your people, and now your life +BEG! for death, and I'll make it quick +This is MY world, you are not welcome in MY world +Your cries for mercy will not move me. +You are a Maggot! hatched from a mutant Maggot egg! +Your death shall mirror your pathetic life! +I've got a blade with your name on it. +Do not trifle with me, worm. +I'm through playing games with you. +Cross me, and DIE. +I will shatter your soul. +Ahh, a fresh crop of, victims. +HaHa, run weaklings, it pleases me. +Your soul is mine! +Can nooone offer me a challenge? +Come over here, I promise I will heal you. +If the Gods had wanted you to live, they would not have created me! +I am going to strangle you with your own frilly training bra! +To the abyss with thee! +I'm going to stick one arm down your throat, one arm up your arse, and twiddle my thumbs in your guts! +Hm hm hm hm hm hm... heh heh heh heh... Ha ha ha ha ha... Hahahahaha... HAHAHAHAHAHAHAHAHAAAAAA!!!!!!!!!!! +Behold! POWER! +Run! Run you little fool! Run as far as your little legs can take you! Make this fun! +Every kill I make leads me on the road to salvation for every murder I have sown. Free me in death! +Say your last words, [AUDIENCE:RACE]. +You DARE challenge me? +Count your steps child, you shall be dead by the tenth. +Not even the power of Armok himself can save you now. +Fool! Perish at my hand! +This is your atonement! +Lavish these final moments, they will be beautiful. +Don't panic. I don't want your bile polluting your flesh. +Bear witness to power incarnate! +You will scream for torture before I'm done with you! +My body SCREAMS for your blood! It is time to drink! +I sharpened my teeth for this? +Imbecile. The time for talk is over. +It does not matter who is right... only who wins. +We shall not be finished... for a long, long time yet. +Cower before my might and I might just crush you quickly. +Call upon all the gods you want. Even they bow before my strength! +Hah! Was that an attack? +You will cry tears of blood today! +This is an act of mercy on my part; I sin so you may be absolved. +Will you not die for me? +Fight with honor! Die with dignity! +Do you not see? You are dead already. +The string of fate has led us to this battle. Prepare yourself. +I can see the insatiable thirst for violence looming within you. Good... +Even if you do not deserve it, I will grant you a fair fight. +Why aren't you running yet? +You are nothing more than gravel beneath my feet. +Away with thee and ye might live to die another die. +It has been awhile since I last feasted on [AUDIENCE:RACE]. +Be a man. Do the right thing. +Mercy is a trait you'd best hope is universal. +Extreme death... +I have waiting long for this moment. Do not disappoint me. +It's time to suffer! +This hand yearns for your soul! +I shall strike you down! +You have already made your final mistake! +Your next move will be your last! +You have not felt suffering yet! +ROAAAAAAAAAAARRRRR!!!! +I will smack your head around like a little ball! +Say hello to my little friend! +Your blood will flow like the crimson sea! +I will purge you from these lands! +No more words! We fight! +You don't believe my might? Then it is time for a live demonstration. +I will rip your beating heart out of your chest! +This will be over soon. +Mortal combat!!!! +You had a chance to turn back long before this. Now you will pay the price for your arrogance. +You are already shivering in fright? Pathetic. +This is your final hour! +I will kill you and the very land you walk on! +Unbridle your fear. This is your last chance to let it show. +You are but a stepping stone in my path to glory. +I see you are wearing your own metal coffin. Makes things easier for both of us. +Fear me, for I am your apocalypse! +It pains me to know that your unclean blood will stain this place. +Your skull had better not be thick enough to nick my weapon. +Listen, [AUDIENCE:RACE]! The whispers of the wind speak only of your death. The world itself begs for your blood! +Once I've killed you, I will hunt down your friends and your family and they will curse your name before they die long and painful deaths. +I grant you a moment to consider your path and repent of your sins. Good. Now you will perish. +If only there were a bounty on your head. Then, I'd soon be a little richer and a LOT happier! +I salute you before your death. +Ugh, you're covered in blood and vomit! I had been hoping to clean my blade on your cloth once it had slaked itself on your blood. +I am destined to be your last foe. +Embrace the end that awaits you! +May the jackals feast on your flesh when I am through with you! +May you find your forebears in the world beyond. +Look into my eyes and see that there is no victory for you. My determination is absolute. You WILL fall here and now and by my hand! +Finally, you have come to me. Know that I am your doom, [AUDIENCE:RACE]. Your training means nothing, for I am better than you. Your strength means nothing, for I am stronger than you. Your speed means nothing for I am faster than you. Your anger, fear and determination will wash against me and turn their flows to your destruction. Hope will die in your heart even as I wrench the life from your body. Yes, worm, I will end you. \ No newline at end of file diff --git a/scripts/umiman/smallthingsreadmes/Engraving_README.txt b/scripts/umiman/misc/Engraving_README.txt similarity index 100% rename from scripts/umiman/smallthingsreadmes/Engraving_README.txt rename to scripts/umiman/misc/Engraving_README.txt diff --git a/scripts/umiman/smallthingsreadmes/dfmod_umiman_smallthings.txt b/scripts/umiman/misc/dfmod_umiman_smallthings.txt similarity index 100% rename from scripts/umiman/smallthingsreadmes/dfmod_umiman_smallthings.txt rename to scripts/umiman/misc/dfmod_umiman_smallthings.txt diff --git a/scripts/umiman/pydwarf.smallthings.py b/scripts/umiman/pydwarf.smallthings.py index 72df1ff..affe7e1 100644 --- a/scripts/umiman/pydwarf.smallthings.py +++ b/scripts/umiman/pydwarf.smallthings.py @@ -1,15 +1,18 @@ -import os import pydwarf import raws -smalldir = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'smallthingsmod') + + +threats_path = pydwarf.rel(__file__, 'data/smallthings/threat.txt') +nofamily_path = pydwarf.rel(__file__, 'data/smallthings/no_family.txt') +small_dir = pydwarf.rel(__file__, 'raw/smallthings') # A bit of esoteric code which makes smallraws only be read once def getsmallraws(): if 'smallraws' not in globals(): - globals()['smallraws'] = raws.dir(path=smalldir, log=pydwarf.log) + globals()['smallraws'] = raws.dir(root=small_dir, log=pydwarf.log) return smallraws @@ -46,19 +49,17 @@ def getsmallraws(): fleshed out descriptions of the things your dwarves like, as well as more varied ones. With five each, it's pretty rare to see the same two twice. Hopefully I don't have any repeating prefstrings. - - Originally created by Umiman. Ported to ModBase by Fieari. Ported again to - PyDwarf by Sophie.''', + ''', compatibility = (pydwarf.df_0_2x, pydwarf.df_0_3x, pydwarf.df_0_40) ) -def prefstring(dfraws): +def prefstring(df): # Get the smallthings ModBase raws, which is where this data will be coming from smallraws = getsmallraws() if not smallraws: return pydwarf.failure('Failed to read smallthings raws.') # Get all creatures smallcreatures = smallraws.allobj('CREATURE') - dfcreaturesdict = dfraws.objdict('CREATURE') + dfcreaturesdict = df.objdict('CREATURE') # Add the new prefstrings failedcreatures = 0 @@ -73,7 +74,10 @@ def prefstring(dfraws): pydwarf.log.debug('Added %d prefstrings to %s.' % (len(prefs), dfcreature)) # All done! - return pydwarf.success('Added prefstrings to %d creatures.' % (len(smallcreatures) - failedcreatures)) + if (len(smallcreatures) - failedcreatures): + return pydwarf.success('Added prefstrings to %d creatures.' % (len(smallcreatures) - failedcreatures)) + else: + return pydwarf.failure('Added prefstrings to no creatures.') @@ -88,27 +92,29 @@ def prefstring(dfraws): Basically, I added maybe 100 or so new engravings you can potentially see on your floors, walls, studded armour, images, and the like. Keep in mind maybe one or two metagame just a tad but it's funny! I swear! - - Originally created by Umiman. Ported to ModBase by Fieari. Ported again to - PyDwarf by Sophie.''', + ''', compatibility = (pydwarf.df_0_2x, pydwarf.df_0_3x, pydwarf.df_0_40) ) -def engraving(dfraws): +def engraving(df): + if 'descriptor_shape_umiman' in df: return pydwarf.failure('File descriptor_shape_umiman already exists.') + # Get the smallthings ModBase raws, which is where this data will be coming from smallraws = getsmallraws() if not smallraws: return pydwarf.failure('Failed to read smallthings raws.') # Get existing words and shapes - dfwordsdict = dfraws.objdict('WORD') - dfshapesdict = dfraws.objdict('SHAPE') + dfwordsdict = df.objdict('WORD') + dfshapesdict = df.objdict('SHAPE') # Add a new file for the new shapes - dfshapesfile = dfraws.addfile(filename='descriptor_shape_umiman') + dfshapesfile = df.add('raw/objects/descriptor_shape_umiman.txt') dfshapesfile.add('OBJECT:DESCRIPTOR_SHAPE') shapesadded = 0 # Add each shape - for smallshape in smallraws['descriptor_shape_standard'].all(exact_value='SHAPE'): + smallshapes = smallraws['descriptor_shape_standard'] + if smallshapes is None: return pydwarf.failure('Failed to find smallthings raws file named descriptor_shape_standard.') + for smallshape in smallshapes.all(exact_value='SHAPE'): if smallshape.args[0] not in dfshapesdict: # Verify that the shape isn't already in the raws pydwarf.log.debug('Adding shape %s...' % smallshape) @@ -140,3 +146,32 @@ def engraving(dfraws): # All done! return pydwarf.success('Added %s new shapes.' % shapesadded) + +@pydwarf.urist( + name = 'umiman.smallthings.speech.threats', + version = '1.0.0', + author = ('Umiman', 'Sophie Kirschner'), + description = '''Awhile back I asked the community to contribute to fill out the + threat.txt which is used in adventurer when someone threatens you. I.E: in vanilla, + when you face a megabeast or someone who has killed a named creature, they will + talk about who they killed and then say, "prepare to die!!!". That's all they said. + Boring. This compilation has some of the best threats (around 150 and counting) + compiled from that thread and should make killing things too proud of their own + achievements a lot more fun. + ''', + compatibility = (pydwarf.df_0_2x, pydwarf.df_0_3x, pydwarf.df_0_40) +) +def threats(df): + df.add(path=threats_path, loc='data/speech', kind=raws.binfile, replace=True) + return pydwarf.success() + +@pydwarf.urist( + name = 'umiman.smallthings.speech.nofamily', + version = '1.0.0', + author = ('Umiman', 'Sophie Kirschner'), + description = '''Adds more dialog options to no_family.txt.''', + compatibility = (pydwarf.df_0_2x, pydwarf.df_0_3x, pydwarf.df_0_40) +) +def nofamily(df): + df.add(path=nofamily_path, loc='data/speech', kind=raws.binfile, replace=True) + return pydwarf.success() diff --git a/scripts/umiman/smallthingsmod/creature_amphibians.txt b/scripts/umiman/raw/smallthings/creature_amphibians.txt similarity index 100% rename from scripts/umiman/smallthingsmod/creature_amphibians.txt rename to scripts/umiman/raw/smallthings/creature_amphibians.txt diff --git a/scripts/umiman/smallthingsmod/creature_annelids.txt b/scripts/umiman/raw/smallthings/creature_annelids.txt similarity index 100% rename from scripts/umiman/smallthingsmod/creature_annelids.txt rename to scripts/umiman/raw/smallthings/creature_annelids.txt diff --git a/scripts/umiman/smallthingsmod/creature_birds.txt b/scripts/umiman/raw/smallthings/creature_birds.txt similarity index 100% rename from scripts/umiman/smallthingsmod/creature_birds.txt rename to scripts/umiman/raw/smallthings/creature_birds.txt diff --git a/scripts/umiman/smallthingsmod/creature_domestic.txt b/scripts/umiman/raw/smallthings/creature_domestic.txt similarity index 100% rename from scripts/umiman/smallthingsmod/creature_domestic.txt rename to scripts/umiman/raw/smallthings/creature_domestic.txt diff --git a/scripts/umiman/smallthingsmod/creature_fanciful.txt b/scripts/umiman/raw/smallthings/creature_fanciful.txt similarity index 100% rename from scripts/umiman/smallthingsmod/creature_fanciful.txt rename to scripts/umiman/raw/smallthings/creature_fanciful.txt diff --git a/scripts/umiman/smallthingsmod/creature_insects.txt b/scripts/umiman/raw/smallthings/creature_insects.txt similarity index 100% rename from scripts/umiman/smallthingsmod/creature_insects.txt rename to scripts/umiman/raw/smallthings/creature_insects.txt diff --git a/scripts/umiman/smallthingsmod/creature_large_mountain.txt b/scripts/umiman/raw/smallthings/creature_large_mountain.txt similarity index 100% rename from scripts/umiman/smallthingsmod/creature_large_mountain.txt rename to scripts/umiman/raw/smallthings/creature_large_mountain.txt diff --git a/scripts/umiman/smallthingsmod/creature_large_ocean.txt b/scripts/umiman/raw/smallthings/creature_large_ocean.txt similarity index 100% rename from scripts/umiman/smallthingsmod/creature_large_ocean.txt rename to scripts/umiman/raw/smallthings/creature_large_ocean.txt diff --git a/scripts/umiman/smallthingsmod/creature_large_riverlake.txt b/scripts/umiman/raw/smallthings/creature_large_riverlake.txt similarity index 100% rename from scripts/umiman/smallthingsmod/creature_large_riverlake.txt rename to scripts/umiman/raw/smallthings/creature_large_riverlake.txt diff --git a/scripts/umiman/smallthingsmod/creature_large_temperate.txt b/scripts/umiman/raw/smallthings/creature_large_temperate.txt similarity index 100% rename from scripts/umiman/smallthingsmod/creature_large_temperate.txt rename to scripts/umiman/raw/smallthings/creature_large_temperate.txt diff --git a/scripts/umiman/smallthingsmod/creature_large_tropical.txt b/scripts/umiman/raw/smallthings/creature_large_tropical.txt similarity index 100% rename from scripts/umiman/smallthingsmod/creature_large_tropical.txt rename to scripts/umiman/raw/smallthings/creature_large_tropical.txt diff --git a/scripts/umiman/smallthingsmod/creature_large_tundra.txt b/scripts/umiman/raw/smallthings/creature_large_tundra.txt similarity index 100% rename from scripts/umiman/smallthingsmod/creature_large_tundra.txt rename to scripts/umiman/raw/smallthings/creature_large_tundra.txt diff --git a/scripts/umiman/smallthingsmod/creature_other.txt b/scripts/umiman/raw/smallthings/creature_other.txt similarity index 100% rename from scripts/umiman/smallthingsmod/creature_other.txt rename to scripts/umiman/raw/smallthings/creature_other.txt diff --git a/scripts/umiman/smallthingsmod/creature_reptiles.txt b/scripts/umiman/raw/smallthings/creature_reptiles.txt similarity index 100% rename from scripts/umiman/smallthingsmod/creature_reptiles.txt rename to scripts/umiman/raw/smallthings/creature_reptiles.txt diff --git a/scripts/umiman/smallthingsmod/creature_savage_tropical.txt b/scripts/umiman/raw/smallthings/creature_savage_tropical.txt similarity index 100% rename from scripts/umiman/smallthingsmod/creature_savage_tropical.txt rename to scripts/umiman/raw/smallthings/creature_savage_tropical.txt diff --git a/scripts/umiman/smallthingsmod/creature_small_mammals.txt b/scripts/umiman/raw/smallthings/creature_small_mammals.txt similarity index 100% rename from scripts/umiman/smallthingsmod/creature_small_mammals.txt rename to scripts/umiman/raw/smallthings/creature_small_mammals.txt diff --git a/scripts/umiman/smallthingsmod/creature_small_ocean.txt b/scripts/umiman/raw/smallthings/creature_small_ocean.txt similarity index 100% rename from scripts/umiman/smallthingsmod/creature_small_ocean.txt rename to scripts/umiman/raw/smallthings/creature_small_ocean.txt diff --git a/scripts/umiman/smallthingsmod/creature_small_riverlake.txt b/scripts/umiman/raw/smallthings/creature_small_riverlake.txt similarity index 100% rename from scripts/umiman/smallthingsmod/creature_small_riverlake.txt rename to scripts/umiman/raw/smallthings/creature_small_riverlake.txt diff --git a/scripts/umiman/smallthingsmod/creature_standard.txt b/scripts/umiman/raw/smallthings/creature_standard.txt similarity index 100% rename from scripts/umiman/smallthingsmod/creature_standard.txt rename to scripts/umiman/raw/smallthings/creature_standard.txt diff --git a/scripts/umiman/smallthingsmod/creature_subterranean.txt b/scripts/umiman/raw/smallthings/creature_subterranean.txt similarity index 100% rename from scripts/umiman/smallthingsmod/creature_subterranean.txt rename to scripts/umiman/raw/smallthings/creature_subterranean.txt diff --git a/scripts/umiman/smallthingsmod/descriptor_shape_standard.txt b/scripts/umiman/raw/smallthings/descriptor_shape_standard.txt similarity index 100% rename from scripts/umiman/smallthingsmod/descriptor_shape_standard.txt rename to scripts/umiman/raw/smallthings/descriptor_shape_standard.txt diff --git a/scripts/umiman/readme.md b/scripts/umiman/readme.md new file mode 100644 index 0000000..eff04c0 --- /dev/null +++ b/scripts/umiman/readme.md @@ -0,0 +1,38 @@ +# Smallthings + +Orignially created by Umiman. (http://www.bay12forums.com/smf/index.php?topic=18552.0) +Rewritten for ModBase by Fieari. (http://dffd.bay12games.com/file.php?id=271) +Rewritten for PyDwarf by Sophie Kirschner. (http://www.pineapplemachine.com) + +A collection of descriptive mods which aim to add more spice to Dwarf Fortress's text and dialogue. + +## engraving + +Has this been done before? While I think it's impossible to change any of the inbuilt engraving stuff like, "this is a picture of a dwarf and a tentacle demon. The dwarf is embracing the tentacle demon", it is possible to edit and add to more basic ones such as "this is a picture of a crescent moon". Basically, I added maybe 200 or so new engravings you can potentially see on your floors, walls, studded armour, images, and the like. Keep in mind maybe one or two metagame just a tad but it's funny! I swear! + +## prefstring + +This mod simply just adds to the number of prefstrings for everything in the game to a minimum of five each. Prefstrings are the stuff that tell your dwarves what to like about a creature. For example, "He likes large roaches for their ability to disgust". With this mod, you'll see more fleshed out descriptions of the things your dwarves like, as well as more varied ones. With five each, it's pretty rare to see the same two twice. Hopefully I don't have any repeating prefstrings + +Plant prefstrings still need to be added. + +Many of these are collected from the community. The philosophy behind each included addition is as follows: + +* The mod should be universal. I.E: if someone were to use this mod in conjunction with others, they should not only be compatible, but not break the illusion. +* The mod should be in the vein of the Dwarf Fortress universe and not break the illusion that you are in it (except in rare cases of comedy). +* The mod should be as descriptively neutral as possible and so work for goblins just as it would for elves or dwarves so as to not break the illusion. +* Try to put things Toady would have put in if he had the time. +* Only focus on what the mod is supposed to do. Hence, my small things mod will not do ANYTHING except add descriptive words and text. +* Maintain the illusion.] + +## speech + +Awhile back I asked the community to contribute to fill out the threat.txt which is used in adventurer when someone threatens you. I.E: in vanilla, when you face a megabeast or someone who has killed a named creature, they will talk about who they killed and then say, "prepare to die!!!". That's all they said. Boring. This compilation has some of the best threats (around 150 and counting) compiled from that thread and should make killing things too proud of their own achievements a lot more fun. + +### nofamily + +Adds dialog options to no_family.txt, the vanilla version of which contains only "I have no family to speak of". + +### threats + +Adds dialog options to threats.txt, the vanilla version of which contains only "Prepare to die!". diff --git a/scripts/umiman/readme.txt b/scripts/umiman/readme.txt deleted file mode 100644 index 8de3ddc..0000000 --- a/scripts/umiman/readme.txt +++ /dev/null @@ -1,22 +0,0 @@ -Smallthings orignially created by Umiman. (http://www.bay12forums.com/smf/index.php?topic=18552.0) -Smallthings ported to ModBase by Fieari. (http://dffd.bay12games.com/file.php?id=271) -Smallthings ModBase version ported to PyDwarf by Sophie Kirschner. (http://www.pineapplemachine.com) - -Creator; umiman and the Bay12games Forum Community -Converted to ModBase format by Fieari - -A collection of descriptive mods which aim to add more spice to Dwarf Fortress's text and dialogue. - -* Prefstring minimod: Adds more flavour text to a dwarf's profile. Dwarves now have at least 5 proper reasons to like something instead of the current basic text. -* Engraving minimod: Adds over 200 new engravings so you won't have to constantly see "This is an engraving of a square" - -Plant prefstrings still need to be added. - -Many of these are collected from the community. The philosophy behind each included addition is as follows: - -1. The mod should be universal. I.E: if someone were to use this mod in conjunction with others, they should not only be compatible, but not break the illusion. -2. The mod should be in the vein of the Dwarf Fortress universe and not break the illusion that you are in it (except in rare cases of comedy). -3. The mod should be as descriptively neutral as possible and so work for goblins just as it would for elves or dwarves so as to not break the illusion. -4. Try to put things Toady would have put in if he had the time. -5. Only focus on what the mod is supposed to do. Hence, my small things mod will not do ANYTHING except add descriptive words and text. -6. Maintain the illusion.]