Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Create first public examples for how to use RichChk #91

Merged
merged 5 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ data/
# temporary ignore these
chkjson-v1/
chkjson-v2/
examples/
examples-dev/
examples/generated-maps
tst/
test/prof

Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ repos:
hooks:
- id: mypy
args: ["--strict", "--ignore-missing-imports"]
exclude: ^test/
exclude: (^test/|^examples/)
additional_dependencies: ['types-PyYAML']
- repo: https://github.com/psf/black
rev: 22.12.0
Expand Down
22 changes: 18 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ RichChk offers numerous advantages over the traditional method of using a GUI ba

* Edit a map directly from Python to a .SCX/.SCM map file. No copy+pasting triggers needed!
* Ergonomically write many triggers in a Python/text based format without using a GUI.
* Leverage all the power of Python/version control to organize triggers, unit settings, etc. outside of a map file for high maintainbility and portability.
* Leverage all the power of Python/version control to organize triggers, unit settings, etc. outside of a map file for high maintainability and portability.
* Unify static data (unit settings, player settings, etc.) with Trigger data. Trigger data can programmatically reference static data when building a map.


Expand Down Expand Up @@ -46,15 +46,29 @@ Example YAML config that sets the log level to `DEBUG`:

```yaml
logging:
level: DEBUG
level: DEBUG
```

Note the above is the only possible configuration supported at the moment (changing the logging level). In the future additional configuration options may be available.

## Usage

## Examples
Specific examples are provided in the [examples/](examples/) top level folder. These showcase the basic operations of a reading a map's CHK data, adding new data, and saving it to a new map. [hello_world.py](examples/hello_world.py) illustrates how to edit unit settings data and display messages. [hyper_triggers.py](examples/hyper_triggers.py) shows how to add hyper triggers to a map and demonstrates this by spawning many Zerglings quickly in the map center.

To use RichChk for map development, it is best to divide a map into two logical divisions:

* A .SCM/.SCX map file containing the terrain, pre-placed units, pre-placed locations, etc. (anything that is best done in a graphical interface)
* A set of RichChk Python scripts that represent the triggers, unit setting data, etc.

Producing a new map typically involves the following steps:

1. Load the map in memory, e.g. `StarcraftMpqIo#read_chk_from_mpq`
1. Edit the desired sections, e.g. to add new triggers use `RichTrigEditor#add_triggers`
1. Create a new CHK with the edited section(s), e.g. `RichChkEditor#replace_chk_section`
1. Save the new CHK to a new map file, e.g. `StarcraftMpqIo#save_chk_to_mpq`

Whenever a map is "edited", a new map file should be created everytime. It is highly discouraged to replace the map file being edited, as this risks loss of data. Existing maps should be viewed as immutable--they cannot be changed, only used to make newer versions. The RichChk file I/O operations have safeguards that prevent overwriting exist files, but these flags can be disabled in each method.

TBD

## Design Philosophy

Expand Down
92 changes: 92 additions & 0 deletions examples/hello_world.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
"""Display a "Hello World!" message in a StarCraft map.

This also sets all unit names to "Hello World!" to demonstrate how to edit unit data.
"""

from decimal import Decimal

from richchk.editor.richchk.rich_chk_editor import RichChkEditor
from richchk.editor.richchk.rich_trig_editor import RichTrigEditor
from richchk.editor.richchk.rich_unix_editor import RichUnixEditor
from richchk.io.mpq.starcraft_mpq_io_helper import StarCraftMpqIoHelper
from richchk.io.richchk.query.chk_query_util import ChkQueryUtil
from richchk.model.richchk.str.rich_string import RichString
from richchk.model.richchk.trig.actions.display_text_message_action import (
DisplayTextMessageAction,
)
from richchk.model.richchk.trig.conditions.always_condition import AlwaysCondition
from richchk.model.richchk.trig.player_id import PlayerId
from richchk.model.richchk.trig.rich_trig_section import RichTrigSection
from richchk.model.richchk.trig.rich_trigger import RichTrigger
from richchk.model.richchk.unis.unit_id import UnitId
from richchk.model.richchk.unis.unit_setting import UnitSetting
from richchk.model.richchk.unis.unit_to_weapon_lookup import get_weapons_for_unit
from richchk.model.richchk.unis.weapon_setting import WeaponSetting
from richchk.model.richchk.unix.rich_unix_section import RichUnixSection

# replace this with the path to the DLL on your local computer
PATH_TO_STORMLIB_DLL = None
INPUT_MAP_FILE = "maps/base-map.scx"
OUTPUT_MAP_FILE = "generated-maps/hello-world-generated.scx"

BLACKLISTED_UNIT_IDS = [
UnitId.ANY_UNIT,
UnitId.NO_UNIT,
UnitId.MEN,
UnitId.FACTORIES,
UnitId.BUILDINGS,
]


def generate_unit_settings() -> list[UnitSetting]:
settings = []
for unit in UnitId:
if unit in BLACKLISTED_UNIT_IDS:
continue
ws = [
WeaponSetting(_weapon_id=weapon, _base_damage=1, _upgrade_damage=1)
for weapon in get_weapons_for_unit(unit)
]
# the default unit settings are not stored in the CHK
# so we must fully replace each value even if we wish to only edit a single setting
us = UnitSetting(
_unit_id=unit,
_hitpoints=Decimal(1),
_shieldpoints=1,
_armorpoints=1,
_build_time=1,
_mineral_cost=1,
_gas_cost=1,
_custom_unit_name=RichString(_value="Hello world!"),
_weapons=ws,
)
settings.append(us)
return settings


def generate_display_hello_world_trigger():
return RichTrigger(
_conditions=[AlwaysCondition()],
_actions=[DisplayTextMessageAction(_text=RichString(_value="Hello world!"))],
_players={PlayerId.ALL_PLAYERS},
)


if __name__ == "__main__":
mpqio = StarCraftMpqIoHelper.create_mpq_io(PATH_TO_STORMLIB_DLL)
chk = mpqio.read_chk_from_mpq(INPUT_MAP_FILE)
new_unit_settings = generate_unit_settings()
new_unix = RichUnixEditor().upsert_all_unit_settings(
new_unit_settings,
ChkQueryUtil.find_only_rich_section_in_chk(RichUnixSection, chk),
)
new_trig = RichTrigEditor().add_triggers(
[generate_display_hello_world_trigger()],
ChkQueryUtil.find_only_rich_section_in_chk(RichTrigSection, chk),
)
new_chk = RichChkEditor().replace_chk_section(
new_trig, RichChkEditor().replace_chk_section(new_unix, chk)
)
mpqio.save_chk_to_mpq(
new_chk, INPUT_MAP_FILE, OUTPUT_MAP_FILE, overwrite_existing=True
)
98 changes: 98 additions & 0 deletions examples/hyper_triggers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
"""Add Hyper Triggers to a map.

This demonstrates how adding hyper triggers makes trigger cycles much faster, causing
units to spawn quicker.

This is based off the discussion here:
http://www.staredit.net/topic/16433/
"""

from richchk.editor.richchk.rich_chk_editor import RichChkEditor
from richchk.editor.richchk.rich_trig_editor import RichTrigEditor
from richchk.io.mpq.starcraft_mpq_io_helper import StarCraftMpqIoHelper
from richchk.io.richchk.query.chk_query_util import ChkQueryUtil
from richchk.io.richchk.query.mrgn_query_util import MrgnQueryUtil
from richchk.model.richchk.mrgn.rich_location import RichLocation
from richchk.model.richchk.mrgn.rich_mrgn_section import RichMrgnSection
from richchk.model.richchk.trig.actions.create_unit_action import CreateUnitAction
from richchk.model.richchk.trig.actions.preserve_trigger_action import PreserveTrigger
from richchk.model.richchk.trig.actions.set_deaths_action import SetDeathsAction
from richchk.model.richchk.trig.actions.wait_trigger_action import WaitAction
from richchk.model.richchk.trig.conditions.always_condition import AlwaysCondition
from richchk.model.richchk.trig.conditions.comparators.numeric_comparator import (
NumericComparator,
)
from richchk.model.richchk.trig.conditions.deaths_condition import DeathsCondition
from richchk.model.richchk.trig.enums.amount_modifier import AmountModifier
from richchk.model.richchk.trig.player_id import PlayerId
from richchk.model.richchk.trig.rich_trig_section import RichTrigSection
from richchk.model.richchk.trig.rich_trigger import RichTrigger
from richchk.model.richchk.unis.unit_id import UnitId

# replace this with the path to the DLL on your local computer
PATH_TO_STORMLIB_DLL = None
INPUT_MAP_FILE = "maps/base-map.scx"
OUTPUT_MAP_FILE = "generated-maps/hyper-triggers-generated.scx"


def create_hyper_triggers():
return RichTrigger(
_conditions=[
DeathsCondition(
_group=PlayerId.ALL_PLAYERS,
_comparator=NumericComparator.EXACTLY,
_amount=0,
_unit=UnitId.CAVE,
)
],
_actions=[
SetDeathsAction(
_group=PlayerId.CURRENT_PLAYER,
_unit=UnitId.CAVE,
_amount=1,
_amount_modifier=AmountModifier.SET_TO,
),
WaitAction(_milliseconds=0),
SetDeathsAction(
_group=PlayerId.CURRENT_PLAYER,
_unit=UnitId.CAVE,
_amount=0,
_amount_modifier=AmountModifier.SET_TO,
),
WaitAction(_milliseconds=0),
PreserveTrigger(),
],
_players={PlayerId.ALL_PLAYERS},
)


def create_unit_spawn_trigger(spawn_location: RichLocation):
return RichTrigger(
_conditions=[AlwaysCondition()],
_actions=[
CreateUnitAction(
_group=PlayerId.ALL_PLAYERS,
_amount=1,
_unit=UnitId.ZERG_ZERGLING,
_location=spawn_location,
),
PreserveTrigger(),
],
_players={PlayerId.ALL_PLAYERS},
)


if __name__ == "__main__":
mpqio = StarCraftMpqIoHelper.create_mpq_io(PATH_TO_STORMLIB_DLL)
chk = mpqio.read_chk_from_mpq(INPUT_MAP_FILE)
anywhere_loc = MrgnQueryUtil.find_location_by_fuzzy_search(
"anywhere", ChkQueryUtil.find_only_rich_section_in_chk(RichMrgnSection, chk)
)
new_trig = RichTrigEditor().add_triggers(
[create_hyper_triggers(), create_unit_spawn_trigger(anywhere_loc)],
ChkQueryUtil.find_only_rich_section_in_chk(RichTrigSection, chk),
)
new_chk = RichChkEditor().replace_chk_section(new_trig, chk)
mpqio.save_chk_to_mpq(
new_chk, INPUT_MAP_FILE, OUTPUT_MAP_FILE, overwrite_existing=True
)
Binary file added examples/maps/base-map.scx
Binary file not shown.
22 changes: 11 additions & 11 deletions src/richchk/editor/richchk/rich_unix_editor.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,51 +15,51 @@ def __init__(self) -> None:
self.log: logging.Logger = logger.get_logger(RichUnixEditor.__name__)

def upsert_all_unit_settings(
self, unit_settings: list[UnitSetting], unis: RichUnixSection
self, unit_settings: list[UnitSetting], unix: RichUnixSection
) -> RichUnixSection:
"""Add or replace the unit settings for each modified unit.

:param self:
:param unit_settings:
:param unis:
:param unix:
:return:
"""
new_unis = copy.deepcopy(unis)
new_unix = copy.deepcopy(unix)
for us in unit_settings:
new_unis = self.upsert_unit_setting(unit_setting=us, unis=new_unis)
return new_unis
new_unix = self.upsert_unit_setting(unit_setting=us, unix=new_unix)
return new_unix

def upsert_unit_setting(
self, unit_setting: UnitSetting, unis: RichUnixSection
self, unit_setting: UnitSetting, unix: RichUnixSection
) -> RichUnixSection:
"""Add or replace the unit settings for a modified unit.

:param unit_setting:
:param unis:
:param unix:
:return:
"""
already_existing_unit_setting = self._find_unit_setting_by_unit_id(
unit_setting.unit_id, unis
unit_setting.unit_id, unix
)
if already_existing_unit_setting:
self.log.info(
f"There is already a unit setting for unit ID {unit_setting.unit_id}. "
f"This will be replaced by the new unit setting."
)
new_unit_settings = self._append_or_replace_unit_setting(
unit_setting, unis.unit_settings
unit_setting, unix.unit_settings
)
return RichUnixSection(_unit_settings=new_unit_settings)

def _find_unit_setting_by_unit_id(
self, unit_id: UnitId, unis: RichUnixSection
self, unit_id: UnitId, unix: RichUnixSection
) -> Optional[UnitSetting]:
"""Find the modified unit setting for a give unit ID.

:param unit_id:
:return:
"""
maybe_unit_settings = [x for x in unis.unit_settings if x.unit_id == unit_id]
maybe_unit_settings = [x for x in unix.unit_settings if x.unit_id == unit_id]
if not maybe_unit_settings:
return None
if len(maybe_unit_settings) > 1:
Expand Down
2 changes: 1 addition & 1 deletion src/setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
setup(
name="richchk",
version="0.1",
description="Parse Starcraft CHK format to a rich, readable, and editable format.",
description="Parse Starcraft CHK format into a rich, readable, and editable format.",
url="https://github.com/sethmachine/richchk",
author="sethmachine",
author_email="sethmachine01@gmail.com",
Expand Down
8 changes: 4 additions & 4 deletions test/editor/richchk/rich_unix_editor_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def test_it_replaces_rich_chk_section():
generate_unit_setting(UnitId.ZERG_ZERGLING),
]
new_unis = editor.upsert_all_unit_settings(
unit_settings=new_settings, unis=empty_rich_unix
unit_settings=new_settings, unix=empty_rich_unix
)
for setting in new_settings:
assert setting in new_unis.unit_settings
Expand All @@ -39,7 +39,7 @@ def test_it_upserts_multiple_unit_settings():
generate_unit_setting(UnitId.ZERG_ZERGLING),
]
new_unis = editor.upsert_all_unit_settings(
unit_settings=new_settings, unis=empty_rich_unix
unit_settings=new_settings, unix=empty_rich_unix
)
for setting in new_settings:
assert setting in new_unis.unit_settings
Expand All @@ -51,7 +51,7 @@ def test_it_upserts_a_single_unit_setting():
editor = RichUnixEditor()
terran_marine_unit_setting = generate_unit_setting(UnitId.TERRAN_MARINE)
new_unis = editor.upsert_unit_setting(
unit_setting=terran_marine_unit_setting, unis=empty_rich_unix
unit_setting=terran_marine_unit_setting, unix=empty_rich_unix
)
assert terran_marine_unit_setting in new_unis.unit_settings
assert terran_marine_unit_setting not in empty_rich_unix.unit_settings
Expand Down Expand Up @@ -82,7 +82,7 @@ def test_it_replaces_unit_setting_if_it_already_exists():
)
assert new_marine not in rich_unix_with_terran_marine_setting.unit_settings
new_unis = editor.upsert_unit_setting(
unit_setting=new_marine, unis=rich_unix_with_terran_marine_setting
unit_setting=new_marine, unix=rich_unix_with_terran_marine_setting
)
assert new_unis.unit_settings == [new_marine]
assert new_marine not in rich_unix_with_terran_marine_setting.unit_settings
Loading