Add literals-mappings to EvForm for custom remaps
This commit is contained in:
parent
f48db64a7c
commit
158b9e2e12
3 changed files with 135 additions and 78 deletions
|
|
@ -210,7 +210,9 @@ Up requirements to Django 4.0+, Twisted 22+, Python 3.9 or 3.10
|
||||||
- Improve search performance when having many DB-based prototypes via caching.
|
- Improve search performance when having many DB-based prototypes via caching.
|
||||||
- Remove the `return_parents` kwarg of `evennia.prototypes.spawner.spawn` since it
|
- Remove the `return_parents` kwarg of `evennia.prototypes.spawner.spawn` since it
|
||||||
was inefficient and unused.
|
was inefficient and unused.
|
||||||
- Made all id fields BigAutoField for all databases.
|
- Made all id fields BigAutoField for all databases. (owllex)
|
||||||
|
- `EvForm` refactored. New `literals` mapping, for literal mappings into the
|
||||||
|
main template (e.g. for single-character replacements).
|
||||||
|
|
||||||
## Evennia 0.9.5
|
## Evennia 0.9.5
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,13 @@
|
||||||
"""
|
"""
|
||||||
EvForm - a way to create advanced ASCII forms
|
EvForm - a way to create advanced ASCII forms
|
||||||
|
|
||||||
This is intended for creating advanced ASCII game forms, such as a
|
This is intended for creating advanced ASCII game forms, such as a large pretty character sheet or
|
||||||
large pretty character sheet or info document.
|
info document.
|
||||||
|
|
||||||
The system works on the basis of a readin template that is given in a
|
The system works on the basis of a readin template that is given in a separate Python file imported
|
||||||
separate Python file imported into the handler. This file contains
|
into the handler. This file contains some optional settings and a string mapping out the form. The
|
||||||
some optional settings and a string mapping out the form. The template
|
template has markers in it to denounce fields to fill. The markers map the absolute size of the
|
||||||
has markers in it to denounce fields to fill. The markers map the
|
field and will be filled with an `evtable.EvCell` object when displaying the form.
|
||||||
absolute size of the field and will be filled with an `evtable.EvCell`
|
|
||||||
object when displaying the form.
|
|
||||||
|
|
||||||
Example of input file `testform.py`:
|
Example of input file `testform.py`:
|
||||||
|
|
||||||
|
|
@ -38,56 +36,68 @@ FORM = '''
|
||||||
| cccccccc | ccccccccccccccccccccccccccccccccccc |
|
| cccccccc | ccccccccccccccccccccccccccccccccccc |
|
||||||
| cccccccc | cccccccccccccccccBccccccccccccccccc |
|
| cccccccc | cccccccccccccccccBccccccccccccccccc |
|
||||||
| | |
|
| | |
|
||||||
|
| v& |
|
||||||
-------------------------------------------------
|
-------------------------------------------------
|
||||||
'''
|
'''
|
||||||
```
|
```
|
||||||
|
|
||||||
The first line of the `FORM` string is ignored. The forms and table
|
The first line of the `FORM` string is ignored if empty. The forms and table markers must mark out
|
||||||
markers must mark out complete, unbroken rectangles, each containing
|
complete, unbroken rectangles, each containing one embedded single-character identifier (so the
|
||||||
one embedded single-character identifier (so the smallest element
|
smallest element possible is a 3-character wide form). The identifier can be any character except
|
||||||
possible is a 3-character wide form). The identifier can be any
|
for the `FORM_CHAR` and `TABLE_CHAR` and some of the common ASCII-art elements, like space, `_` `|`
|
||||||
character except for the `FORM_CHAR` and `TABLE_CHAR` and some of the
|
`*` etc (see `INVALID_FORMCHARS` in this module). Form Rectangles can have any size, but must be
|
||||||
common ASCII-art elements, like space, `_` `|` `*` etc (see
|
separated from each other by at least one other character's width.
|
||||||
`INVALID_FORMCHARS` in this module). Form Rectangles can have any size,
|
|
||||||
but must be separated from each other by at least one other
|
|
||||||
character's width.
|
|
||||||
|
|
||||||
|
The form can also replace literal markers not abiding by these rules. For example, the `v&` in the
|
||||||
|
bottom right corner could be such literal marker. If a literal-mapping for 'v&' is provided, all
|
||||||
|
occurrences of this marker will be replaced. This will happen *before* any other parsing, so in
|
||||||
|
principle this could be used to inject new fields/tables into the form dynamically. This literal
|
||||||
|
mapping does not consider width, but it will affect to total width of the form, so make sure what
|
||||||
|
you inject does not break things. Using literal markers is the only way to inject 1 or 2-character
|
||||||
|
replacements.
|
||||||
|
|
||||||
Use as follows:
|
Usage
|
||||||
|
|
||||||
```python
|
```python
|
||||||
from evennia import EvForm, EvTable
|
from evennia import EvForm, EvTable
|
||||||
|
|
||||||
# create a new form from the template
|
# create a new form from the template
|
||||||
form = EvForm("path/to/testform.py")
|
form = EvForm("path/to/testform.py")
|
||||||
|
|
||||||
# EvForm can also take a dictionary instead of a filepath, as long
|
# alteratively, you can supply the template as a dict:
|
||||||
# as the dict contains the keys FORMCHAR, TABLECHAR and FORM
|
|
||||||
# form = EvForm(form=form_dict)
|
|
||||||
|
|
||||||
# add data to each tagged form cell
|
form = EvForm({"FORM": "....", "TABLECHAR": "c", "FORMCHAR": "x"})
|
||||||
form.map(cells={1: "Tom the Bouncer",
|
|
||||||
2: "Griatch",
|
|
||||||
3: "A sturdy fellow",
|
|
||||||
4: 12,
|
|
||||||
5: 10,
|
|
||||||
6: 5,
|
|
||||||
7: 18,
|
|
||||||
8: 10,
|
|
||||||
9: 3})
|
|
||||||
# create the EvTables
|
|
||||||
tableA = EvTable("HP","MV","MP",
|
|
||||||
table=[["**"], ["*****"], ["***"]],
|
|
||||||
border="incols")
|
|
||||||
tableB = EvTable("Skill", "Value", "Exp",
|
|
||||||
table=[["Shooting", "Herbalism", "Smithing"],
|
|
||||||
[12,14,9],["550/1200", "990/1400", "205/900"]],
|
|
||||||
border="incols")
|
|
||||||
# add the tables to the proper ids in the form
|
|
||||||
form.map(tables={"A": tableA,
|
|
||||||
"B": tableB})
|
|
||||||
|
|
||||||
print(form)
|
# EvForm can also take a dictionary instead of a filepath, as long
|
||||||
|
# as the dict contains the keys FORMCHAR, TABLECHAR and FORM
|
||||||
|
# form = EvForm(form=form_dict)
|
||||||
|
|
||||||
|
# add data to each tagged form cell
|
||||||
|
form.map(cells={1: "Tom the Bouncer",
|
||||||
|
2: "Griatch",
|
||||||
|
3: "A sturdy fellow",
|
||||||
|
4: 12,
|
||||||
|
5: 10,
|
||||||
|
6: 5,
|
||||||
|
7: 18,
|
||||||
|
8: 10,
|
||||||
|
9: 3})
|
||||||
|
# create the EvTables
|
||||||
|
tableA = EvTable("HP","MV","MP",
|
||||||
|
table=[["**"], ["*****"], ["***"]],
|
||||||
|
border="incols")
|
||||||
|
tableB = EvTable("Skill", "Value", "Exp",
|
||||||
|
table=[["Shooting", "Herbalism", "Smithing"],
|
||||||
|
[12,14,9],["550/1200", "990/1400", "205/900"]],
|
||||||
|
border="incols")
|
||||||
|
# map 'literal' replacents (here, a version string)
|
||||||
|
custom_mapping = {"v&", "v2"}
|
||||||
|
|
||||||
|
# add the tables to the proper ids in the form
|
||||||
|
form.map(tables={"A": tableA,
|
||||||
|
"B": tableB})
|
||||||
|
|
||||||
|
print(form)
|
||||||
```
|
```
|
||||||
|
|
||||||
This produces the following result:
|
This produces the following result:
|
||||||
|
|
@ -113,27 +123,30 @@ This produces the following result:
|
||||||
| |**|* | Herbalism |14 |990/1400 |
|
| |**|* | Herbalism |14 |990/1400 |
|
||||||
| |* | | Smithing |9 |205/900 |
|
| |* | | Smithing |9 |205/900 |
|
||||||
| | |
|
| | |
|
||||||
|
| v2 |
|
||||||
------------------------------------------------
|
------------------------------------------------
|
||||||
|
|
||||||
The marked forms have been replaced with EvCells of text and with
|
The marked forms have been replaced with EvCells of text and with EvTables. The literal marker `v&`
|
||||||
EvTables. The form can be updated by simply re-applying `form.map()`
|
was replaced with `v2`.
|
||||||
with the updated data.
|
|
||||||
|
|
||||||
When working with the template ASCII file, you can use `form.reload()`
|
If you change the form layout on disk, you can use `form.reload()` to re-read it from disk without
|
||||||
to re-read the template and re-apply all existing mappings.
|
creating a new form.
|
||||||
|
|
||||||
Each component is restrained to the width and height specified by the
|
If you want to update the data of an existing form, you can use `form.map()` with the changes - the
|
||||||
template, so it will resize to fit (or crop text if the area is too
|
mappings will be updated, keeping the things you want. You can also update the template itself this
|
||||||
small for it). If you try to fit a table into an area it cannot fit
|
way, by supplying it as a dict.
|
||||||
into (when including its borders and at least one line of text), the
|
|
||||||
form will raise an error.
|
Each component (except literal mappings) is restrained to the width and height specified by the
|
||||||
|
template, so it will resize to fit (or crop text if the area is too small for it). If you try to fit
|
||||||
|
a table into an area it cannot fit into (when including its borders and at least one line of text),
|
||||||
|
the form will raise an error.
|
||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import copy
|
|
||||||
import re
|
import re
|
||||||
|
from copy import copy
|
||||||
|
|
||||||
from evennia.utils.ansi import ANSIString
|
from evennia.utils.ansi import ANSIString
|
||||||
from evennia.utils.evtable import EvCell, EvTable
|
from evennia.utils.evtable import EvCell, EvTable
|
||||||
|
|
@ -142,7 +155,6 @@ from evennia.utils.utils import all_from_module, is_iter, to_str
|
||||||
# non-valid form-identifying characters (which can thus be
|
# non-valid form-identifying characters (which can thus be
|
||||||
# used as separators between forms without being detected
|
# used as separators between forms without being detected
|
||||||
# as an identifier). These should be listed in regex form.
|
# as an identifier). These should be listed in regex form.
|
||||||
|
|
||||||
INVALID_FORMCHARS = r"\s\/\|\\\*\_\-\#\<\>\~\^\:\;\.\,"
|
INVALID_FORMCHARS = r"\s\/\|\\\*\_\-\#\<\>\~\^\:\;\.\,"
|
||||||
# if there is an ansi-escape (||) we have to replace this with ||| to make sure
|
# if there is an ansi-escape (||) we have to replace this with ||| to make sure
|
||||||
# to properly escape down the line
|
# to properly escape down the line
|
||||||
|
|
@ -180,7 +192,7 @@ class EvForm:
|
||||||
"enforce_size": True,
|
"enforce_size": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(self, data=None, cells=None, tables=None, **kwargs):
|
def __init__(self, data=None, cells=None, tables=None, literals=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Initiate the form
|
Initiate the form
|
||||||
|
|
||||||
|
|
@ -190,8 +202,13 @@ class EvForm:
|
||||||
also works, to stay compatible with the in-file names). While "form/FORM"
|
also works, to stay compatible with the in-file names). While "form/FORM"
|
||||||
is required, if FORMCHAR/TABLECHAR are not given, they will default to
|
is required, if FORMCHAR/TABLECHAR are not given, they will default to
|
||||||
'x' and 'c' respectively.
|
'x' and 'c' respectively.
|
||||||
cells (dict): A dictionary mapping `{id: text}`
|
cells (dict): A dictionary mapping `{id: str}`
|
||||||
tables (dict): A dictionary mapping `{id: EvTable}`.
|
tables (dict): A dictionary mapping `{id: EvTable}`.
|
||||||
|
literals (dict): A dictionary mapping `{id: str}`. Will be replaced
|
||||||
|
after width of form is calculated, but before cells/tables are mapped.
|
||||||
|
All occurrences of the identifier on the form will be replaced. Note
|
||||||
|
that there is no length-restriction on the remap, you are responsible
|
||||||
|
for not breaking the form.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
Other kwargs are fed as options to the EvCells and EvTables
|
Other kwargs are fed as options to the EvCells and EvTables
|
||||||
|
|
@ -207,10 +224,16 @@ class EvForm:
|
||||||
self.tables_mapping = (
|
self.tables_mapping = (
|
||||||
dict((to_str(key), value) for key, value in tables.items()) if tables else {}
|
dict((to_str(key), value) for key, value in tables.items()) if tables else {}
|
||||||
)
|
)
|
||||||
|
self.literals_mapping = (
|
||||||
|
dict((to_str(key), to_str(value)) for key, value in literals.items())
|
||||||
|
if literals
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
|
||||||
# work arrays
|
# work arrays
|
||||||
|
self.literal_form = ""
|
||||||
self.mapping = {}
|
self.mapping = {}
|
||||||
self.raw_form = []
|
self.matrix = []
|
||||||
self.form = []
|
self.form = []
|
||||||
|
|
||||||
# will parse and build the form
|
# will parse and build the form
|
||||||
|
|
@ -286,7 +309,18 @@ class EvForm:
|
||||||
|
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
def _parse_to_raw_form(self):
|
def _do_literal_mapping(self):
|
||||||
|
"""
|
||||||
|
Do literal replacement in the EvForm.
|
||||||
|
|
||||||
|
"""
|
||||||
|
literal_form = copy(self.data["form"])
|
||||||
|
|
||||||
|
for key, repl in self.literals_mapping.items():
|
||||||
|
literal_form = literal_form.replace(key, repl)
|
||||||
|
return literal_form
|
||||||
|
|
||||||
|
def _parse_to_matrix(self):
|
||||||
"""
|
"""
|
||||||
Forces all lines to be as long as the longest line, filling with whitespace.
|
Forces all lines to be as long as the longest line, filling with whitespace.
|
||||||
|
|
||||||
|
|
@ -298,13 +332,13 @@ class EvForm:
|
||||||
same length as the longest input line
|
same length as the longest input line
|
||||||
|
|
||||||
"""
|
"""
|
||||||
raw_form = EvForm._to_ansi(self.data["form"].split("\n"))
|
matrix = EvForm._to_ansi(self.literal_form.split("\n"))
|
||||||
maxl = max(len(line) for line in raw_form)
|
maxl = max(len(line) for line in matrix)
|
||||||
raw_form = [line + " " * (maxl - len(line)) for line in raw_form]
|
matrix = [line + " " * (maxl - len(line)) for line in matrix]
|
||||||
if raw_form and not raw_form[0].strip():
|
if matrix and not matrix[0].strip():
|
||||||
# the first line is normally empty, we strip it.
|
# the first line is normally empty, we strip it.
|
||||||
raw_form = raw_form[1:]
|
matrix = matrix[1:]
|
||||||
return raw_form
|
return matrix
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _to_ansi(obj, regexable=False):
|
def _to_ansi(obj, regexable=False):
|
||||||
|
|
@ -336,12 +370,12 @@ class EvForm:
|
||||||
"""
|
"""
|
||||||
formchar = self.data["formchar"]
|
formchar = self.data["formchar"]
|
||||||
tablechar = self.data["tablechar"]
|
tablechar = self.data["tablechar"]
|
||||||
form = self.raw_form
|
form = self.matrix
|
||||||
|
|
||||||
cell_options = copy.copy(self.cell_options)
|
cell_options = copy(self.cell_options)
|
||||||
cell_options.update(self.options)
|
cell_options.update(self.options)
|
||||||
|
|
||||||
table_options = copy.copy(self.table_options)
|
table_options = copy(self.table_options)
|
||||||
table_options.update(self.options)
|
table_options.update(self.options)
|
||||||
|
|
||||||
nform = len(form)
|
nform = len(form)
|
||||||
|
|
@ -426,7 +460,7 @@ class EvForm:
|
||||||
the final result.
|
the final result.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
form = copy.copy(self.raw_form)
|
form = copy(self.matrix)
|
||||||
mapping = self.mapping
|
mapping = self.mapping
|
||||||
|
|
||||||
for key, (y, x, width, height, cell_or_table) in mapping.items():
|
for key, (y, x, width, height, cell_or_table) in mapping.items():
|
||||||
|
|
@ -454,14 +488,16 @@ class EvForm:
|
||||||
"""
|
"""
|
||||||
self.data = self._parse_indata()
|
self.data = self._parse_indata()
|
||||||
|
|
||||||
|
# Map any literals into the string
|
||||||
|
self.literal_form = self._do_literal_mapping()
|
||||||
# Create raw form matrix, indexable with (y, x) coords
|
# Create raw form matrix, indexable with (y, x) coords
|
||||||
self.raw_form = self._parse_to_raw_form()
|
self.matrix = self._parse_to_matrix()
|
||||||
# parse and identify all rectangles in the form
|
# parse and identify all rectangles in the form
|
||||||
self.mapping = self._rectangles_to_mapping()
|
self.mapping = self._rectangles_to_mapping()
|
||||||
# combine mapping with form template into a final result
|
# combine mapping with form template into a final result
|
||||||
self.form = self._build_form()
|
self.form = self._build_form()
|
||||||
|
|
||||||
def map(self, cells=None, tables=None, data=None, **kwargs):
|
def map(self, cells=None, tables=None, data=None, literals=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Add mapping for form. This allows for updating an existing
|
Add mapping for form. This allows for updating an existing
|
||||||
evform.
|
evform.
|
||||||
|
|
@ -474,6 +510,7 @@ class EvForm:
|
||||||
data (str or dict): A path to a evform module or a dict with
|
data (str or dict): A path to a evform module or a dict with
|
||||||
the needed "FORM", "TABLE/FORMCHAR" keys. Will replace
|
the needed "FORM", "TABLE/FORMCHAR" keys. Will replace
|
||||||
the originally initialized form.
|
the originally initialized form.
|
||||||
|
literals
|
||||||
|
|
||||||
Keyword Args:
|
Keyword Args:
|
||||||
These will be appended to the existing cell/table options.
|
These will be appended to the existing cell/table options.
|
||||||
|
|
@ -488,9 +525,15 @@ class EvForm:
|
||||||
self.indata = data
|
self.indata = data
|
||||||
|
|
||||||
new_cells = dict((to_str(key), value) for key, value in cells.items()) if cells else {}
|
new_cells = dict((to_str(key), value) for key, value in cells.items()) if cells else {}
|
||||||
new_tables = dict((to_str(key), value) for key, value in tables.items()) if tables else {}
|
|
||||||
self.cells_mapping.update(new_cells)
|
self.cells_mapping.update(new_cells)
|
||||||
|
new_tables = dict((to_str(key), value) for key, value in tables.items()) if tables else {}
|
||||||
self.tables_mapping.update(new_tables)
|
self.tables_mapping.update(new_tables)
|
||||||
|
new_literals = (
|
||||||
|
dict((to_str(key), to_str(value)) for key, value in literals.items())
|
||||||
|
if literals
|
||||||
|
else {}
|
||||||
|
)
|
||||||
|
self.literals_mapping.update(new_literals)
|
||||||
|
|
||||||
self.options.update(self._parse_inkwargs(**kwargs))
|
self.options.update(self._parse_inkwargs(**kwargs))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -48,10 +48,10 @@ class TestEvForm(TestCase):
|
||||||
form.map(tables={"A": tableA, "B": tableB})
|
form.map(tables={"A": tableA, "B": tableB})
|
||||||
return str(form)
|
return str(form)
|
||||||
|
|
||||||
def _simple_form(self, form):
|
def _simple_form(self, form, literals=None):
|
||||||
cellsdict = {1: "Apple", 2: "Banana", 3: "Citrus", 4: "Durian"}
|
cellsdict = {1: "Apple", 2: "Banana", 3: "Citrus", 4: "Durian"}
|
||||||
formdict = {"FORMCHAR": "x", "TABLECHAR": "c", "FORM": form}
|
formdict = {"FORMCHAR": "x", "TABLECHAR": "c", "FORM": form}
|
||||||
form = evform.EvForm(formdict)
|
form = evform.EvForm(formdict, literals=literals)
|
||||||
form.map(cells=cellsdict)
|
form.map(cells=cellsdict)
|
||||||
form = ansi.strip_ansi(str(form))
|
form = ansi.strip_ansi(str(form))
|
||||||
# this is necessary since editors/black tend to strip lines spaces
|
# this is necessary since editors/black tend to strip lines spaces
|
||||||
|
|
@ -166,6 +166,18 @@ Durian
|
||||||
result = self._simple_form(form)
|
result = self._simple_form(form)
|
||||||
self.assertEqual(expected, result)
|
self.assertEqual(expected, result)
|
||||||
|
|
||||||
|
def test_literal_replacement(self):
|
||||||
|
form = """
|
||||||
|
xxxx1xxxx xxxx2xxxx xxxx3xxxx
|
||||||
|
xxxx4xxxx v&
|
||||||
|
"""
|
||||||
|
expected = """
|
||||||
|
Apple Banana Citrus
|
||||||
|
Durian v2
|
||||||
|
""".lstrip()
|
||||||
|
result = self._simple_form(form, literals={"v&": "v2"})
|
||||||
|
self.assertEqual(expected, result)
|
||||||
|
|
||||||
|
|
||||||
# test of issue #2308
|
# test of issue #2308
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue