|
29 | 29 | if typing.TYPE_CHECKING:
|
30 | 30 | import tkinter
|
31 | 31 | import pathlib
|
| 32 | + from BaseClasses import Region |
32 | 33 |
|
33 | 34 |
|
34 | 35 | def tuplize_version(version: str) -> Version:
|
@@ -766,3 +767,113 @@ def freeze_support() -> None:
|
766 | 767 | import multiprocessing
|
767 | 768 | _extend_freeze_support()
|
768 | 769 | multiprocessing.freeze_support()
|
| 770 | + |
| 771 | + |
| 772 | +def visualize_regions(root_region: Region, file_name: str, *, |
| 773 | + show_entrance_names: bool = False, show_locations: bool = True, show_other_regions: bool = True, |
| 774 | + linetype_ortho: bool = True) -> None: |
| 775 | + """Visualize the layout of a world as a PlantUML diagram. |
| 776 | +
|
| 777 | + :param root_region: The region from which to start the diagram from. (Usually the "Menu" region of your world.) |
| 778 | + :param file_name: The name of the destination .puml file. |
| 779 | + :param show_entrance_names: (default False) If enabled, the name of the entrance will be shown near each connection. |
| 780 | + :param show_locations: (default True) If enabled, the locations will be listed inside each region. |
| 781 | + Priority locations will be shown in bold. |
| 782 | + Excluded locations will be stricken out. |
| 783 | + Locations without ID will be shown in italics. |
| 784 | + Locked locations will be shown with a padlock icon. |
| 785 | + For filled locations, the item name will be shown after the location name. |
| 786 | + Progression items will be shown in bold. |
| 787 | + Items without ID will be shown in italics. |
| 788 | + :param show_other_regions: (default True) If enabled, regions that can't be reached by traversing exits are shown. |
| 789 | + :param linetype_ortho: (default True) If enabled, orthogonal straight line parts will be used; otherwise polylines. |
| 790 | +
|
| 791 | + Example usage in World code: |
| 792 | + from Utils import visualize_regions |
| 793 | + visualize_regions(self.multiworld.get_region("Menu", self.player), "my_world.puml") |
| 794 | +
|
| 795 | + Example usage in Main code: |
| 796 | + from Utils import visualize_regions |
| 797 | + for player in world.player_ids: |
| 798 | + visualize_regions(world.get_region("Menu", player), f"{world.get_out_file_name_base(player)}.puml") |
| 799 | + """ |
| 800 | + assert root_region.multiworld, "The multiworld attribute of root_region has to be filled" |
| 801 | + from BaseClasses import Entrance, Item, Location, LocationProgressType, MultiWorld, Region |
| 802 | + from collections import deque |
| 803 | + import re |
| 804 | + |
| 805 | + uml: typing.List[str] = list() |
| 806 | + seen: typing.Set[Region] = set() |
| 807 | + regions: typing.Deque[Region] = deque((root_region,)) |
| 808 | + multiworld: MultiWorld = root_region.multiworld |
| 809 | + |
| 810 | + def fmt(obj: Union[Entrance, Item, Location, Region]) -> str: |
| 811 | + name = obj.name |
| 812 | + if isinstance(obj, Item): |
| 813 | + name = multiworld.get_name_string_for_object(obj) |
| 814 | + if obj.advancement: |
| 815 | + name = f"**{name}**" |
| 816 | + if obj.code is None: |
| 817 | + name = f"//{name}//" |
| 818 | + if isinstance(obj, Location): |
| 819 | + if obj.progress_type == LocationProgressType.PRIORITY: |
| 820 | + name = f"**{name}**" |
| 821 | + elif obj.progress_type == LocationProgressType.EXCLUDED: |
| 822 | + name = f"--{name}--" |
| 823 | + if obj.address is None: |
| 824 | + name = f"//{name}//" |
| 825 | + return re.sub("[\".:]", "", name) |
| 826 | + |
| 827 | + def visualize_exits(region: Region) -> None: |
| 828 | + for exit_ in region.exits: |
| 829 | + if exit_.connected_region: |
| 830 | + if show_entrance_names: |
| 831 | + uml.append(f"\"{fmt(region)}\" --> \"{fmt(exit_.connected_region)}\" : \"{fmt(exit_)}\"") |
| 832 | + else: |
| 833 | + try: |
| 834 | + uml.remove(f"\"{fmt(exit_.connected_region)}\" --> \"{fmt(region)}\"") |
| 835 | + uml.append(f"\"{fmt(exit_.connected_region)}\" <--> \"{fmt(region)}\"") |
| 836 | + except ValueError: |
| 837 | + uml.append(f"\"{fmt(region)}\" --> \"{fmt(exit_.connected_region)}\"") |
| 838 | + else: |
| 839 | + uml.append(f"circle \"unconnected exit:\\n{fmt(exit_)}\"") |
| 840 | + uml.append(f"\"{fmt(region)}\" --> \"unconnected exit:\\n{fmt(exit_)}\"") |
| 841 | + |
| 842 | + def visualize_locations(region: Region) -> None: |
| 843 | + any_lock = any(location.locked for location in region.locations) |
| 844 | + for location in region.locations: |
| 845 | + lock = "<&lock-locked> " if location.locked else "<&lock-unlocked,color=transparent> " if any_lock else "" |
| 846 | + if location.item: |
| 847 | + uml.append(f"\"{fmt(region)}\" : {{method}} {lock}{fmt(location)}: {fmt(location.item)}") |
| 848 | + else: |
| 849 | + uml.append(f"\"{fmt(region)}\" : {{field}} {lock}{fmt(location)}") |
| 850 | + |
| 851 | + def visualize_region(region: Region) -> None: |
| 852 | + uml.append(f"class \"{fmt(region)}\"") |
| 853 | + if show_locations: |
| 854 | + visualize_locations(region) |
| 855 | + visualize_exits(region) |
| 856 | + |
| 857 | + def visualize_other_regions() -> None: |
| 858 | + if other_regions := [region for region in multiworld.get_regions(root_region.player) if region not in seen]: |
| 859 | + uml.append("package \"other regions\" <<Cloud>> {") |
| 860 | + for region in other_regions: |
| 861 | + uml.append(f"class \"{fmt(region)}\"") |
| 862 | + uml.append("}") |
| 863 | + |
| 864 | + uml.append("@startuml") |
| 865 | + uml.append("hide circle") |
| 866 | + uml.append("hide empty members") |
| 867 | + if linetype_ortho: |
| 868 | + uml.append("skinparam linetype ortho") |
| 869 | + while regions: |
| 870 | + if (current_region := regions.popleft()) not in seen: |
| 871 | + seen.add(current_region) |
| 872 | + visualize_region(current_region) |
| 873 | + regions.extend(exit_.connected_region for exit_ in current_region.exits if exit_.connected_region) |
| 874 | + if show_other_regions: |
| 875 | + visualize_other_regions() |
| 876 | + uml.append("@enduml") |
| 877 | + |
| 878 | + with open(file_name, "wt", encoding="utf-8") as f: |
| 879 | + f.write("\n".join(uml)) |
0 commit comments