Merge pull request #30 from Catobat/GridImprovements

Grid Layout Shuffle Customizer and improvements
This commit is contained in:
codemann8
2026-03-15 10:42:53 -05:00
committed by GitHub
31 changed files with 1920 additions and 335 deletions

View File

@@ -567,7 +567,7 @@ def link_overworld(world, player):
remove_connected(forward_edge_sets, back_edge_sets) remove_connected(forward_edge_sets, back_edge_sets)
assert len(connected_edges) == len(default_connections) * 2, connected_edges assert len(connected_edges) == len(default_connections) * 2, connected_edges
valid_layout = validate_layout(world, player) valid_layout = world.accessibility[player] == 'none' or validate_layout(world, player)
tries -= 1 tries -= 1
assert valid_layout, 'Could not find a valid OW layout' assert valid_layout, 'Could not find a valid OW layout'
@@ -1369,9 +1369,6 @@ def build_accessible_region_list(world, start_region, player, build_copy_world=F
return explored_regions return explored_regions
def validate_layout(world, player): def validate_layout(world, player):
if world.accessibility[player] == 'none':
return True
entrance_connectors = { entrance_connectors = {
'East Death Mountain (Bottom)': ['East Death Mountain (Top East)'], 'East Death Mountain (Bottom)': ['East Death Mountain (Top East)'],
'Kakariko Suburb Area': ['Maze Race Ledge'], 'Kakariko Suburb Area': ['Maze Race Ledge'],
@@ -1458,7 +1455,7 @@ def validate_layout(world, player):
while unreachable_count != len(unreachable_regions): while unreachable_count != len(unreachable_regions):
# find unreachable regions # find unreachable regions
unreachable_regions = {} unreachable_regions = {}
for region_name in list(OWTileRegions.copy().keys()): for region_name in list(OWTileRegions.keys()):
if region_name not in explored_regions and region_name not in isolated_regions: if region_name not in explored_regions and region_name not in isolated_regions:
region = world.get_region(region_name, player) region = world.get_region(region_name, player)
unreachable_regions[region_name] = region unreachable_regions[region_name] = region
@@ -1501,9 +1498,55 @@ def validate_layout(world, player):
if len(unreachable_regions): if len(unreachable_regions):
return False return False
return True return True
def get_separate_ow_areas(world, player):
"""
Returns a list of separated areas in the overworld layout.
It looks at the distinct connected components when only considering
OW edge and whirlpool connections (no entrances, portals, mirror, or flute).
Uses Union-Find to handle directed edges properly (treats them as undirected).
"""
parent = {}
def find(x):
if x not in parent:
parent[x] = x
if parent[x] != x:
parent[x] = find(parent[x]) # Path compression
return parent[x]
def union(x, y):
root_x = find(x)
root_y = find(y)
if root_x != root_y:
parent[root_y] = root_x
all_regions = set(OWTileRegions.keys()) - set(isolated_regions)
considered_exit_spot_types = set(['OpenTerrain', 'OWTerrain', 'Ledge', 'OWEdge', 'Whirlpool'])
# Initialize all regions in Union-Find
for region_name in all_regions:
find(region_name)
# Build connections by examining all edges (treating directed as undirected)
for region_name in all_regions:
region = world.get_region(region_name, player)
for exit in region.exits:
if exit.spot_type in considered_exit_spot_types and exit.connected_region is not None and exit.connected_region.name in all_regions:
union(region_name, exit.connected_region.name)
# Group regions by their root
areas = {}
for region_name in all_regions:
root = find(region_name)
if root not in areas:
areas[root] = []
areas[root].append(region_name)
return list(areas.values())
test_connections = [ test_connections = [
#('Links House ES', 'Octoballoon WS'), #('Links House ES', 'Octoballoon WS'),
#('Links House NE', 'Lost Woods Pass SW') #('Links House NE', 'Lost Woods Pass SW')

View File

@@ -277,10 +277,45 @@ someDescription:
`grid` contains additional options that only have an effect when `ow_layout` is set to `grid`. `grid` contains additional options that only have an effect when `ow_layout` is set to `grid`.
#### fixed_arrangements
Use this to dictate the relative positioning between multiple screens (or quadrants of large screens). Screens and quadrants are addressed by their OW Slot ID (independently of their world), ranging from 0x00 to 0x3F. An `arrangement` is given as a list of rows with equal lenghts. If you do not want to specify a full rectangle of screens, you can use `.` as a placeholder to allow the generator to place any screen there. The `world` property can be set to `light`, `dark` or `both` (default value) and determines for which worlds the arrangement applies.
This example forces Death Mountain to stay connected the same as vanilla in both worlds:
```
fixed_arrangements:
- arrangement:
- 0x03 0x04 0x05 0x06 0x07
- 0x0B 0x0C 0x0D 0x0E .
world: both
```
#### restricted_positions
Use this to restrict cells to a specified set of possible positions. The `world` property can be set to `light`, `dark` or `both` (default value) and determines for which worlds the restriction applies.
This example forces the Sanctuary and Link's House screens in both worlds to get placed in corners of the grid:
```
restricted_positions:
- cells:
- 0x13
- 0x2C
positions:
- 0x00
- 0x07
- 0x38
- 0x3F
world: both
```
#### wrap_horizontal / wrap_vertical #### wrap_horizontal / wrap_vertical
Set these to `true` to allow for overworld edge transitions to wrap from one side of a world to the opposite side. With `wrap_horizontal`, there can be east transitions on the eastern edge of the world map that send the player to the western edge of the world. With `wrap_vertical`, there can be south transitions on the southern edge of the world map that send the player to the northern edge of the world. Set these to `true` to allow for overworld edge transitions to wrap from one side of a world to the opposite side. With `wrap_horizontal`, there can be east transitions on the eastern edge of the world map that send the player to the western edge of the world. With `wrap_vertical`, there can be south transitions on the southern edge of the world map that send the player to the northern edge of the world.
#### split_large_screens
When set to `true`, the four quadrants of each large screen are placed on the grid independently of each other.
### ow-crossed ### ow-crossed
This must be defined by player. Each player number should be listed with the appropriate sections and each of these players MUST have `ow_crossed` enabled in the `settings` section in order for any values here to take effect. This section has four primary subsections: `force_crossed`, `force_noncrossed`, `limit_crossed`, and `undefined_chance`. There are also This must be defined by player. Each player number should be listed with the appropriate sections and each of these players MUST have `ow_crossed` enabled in the `settings` section in order for any values here to take effect. This section has four primary subsections: `force_crossed`, `force_noncrossed`, `limit_crossed`, and `undefined_chance`. There are also

View File

@@ -1,5 +1,6 @@
meta: settings:
players: 1 1:
ow_layout: wild
ow-edges: ow-edges:
1: 1:
two-way: two-way:

View File

@@ -0,0 +1,140 @@
settings:
1:
ow_layout: grid
ow-grid:
1:
restricted_positions:
- cells:
- 0x00
- 0x02
- 0x04
- 0x06
- 0x09
- 0x0B
- 0x0D
- 0x0F
- 0x10
- 0x12
- 0x14
- 0x16
- 0x19
- 0x1B
- 0x1D
- 0x1F
- 0x20
- 0x22
- 0x24
- 0x26
- 0x29
- 0x2B
- 0x2D
- 0x2F
- 0x30
- 0x32
- 0x34
- 0x36
- 0x39
- 0x3B
- 0x3D
- 0x3F
positions:
- 0x00
- 0x02
- 0x04
- 0x06
- 0x09
- 0x0B
- 0x0D
- 0x0F
- 0x10
- 0x12
- 0x14
- 0x16
- 0x19
- 0x1B
- 0x1D
- 0x1F
- 0x20
- 0x22
- 0x24
- 0x26
- 0x29
- 0x2B
- 0x2D
- 0x2F
- 0x30
- 0x32
- 0x34
- 0x36
- 0x39
- 0x3B
- 0x3D
- 0x3F
world: both
- cells:
- 0x01
- 0x03
- 0x05
- 0x07
- 0x08
- 0x0A
- 0x0C
- 0x0E
- 0x11
- 0x13
- 0x15
- 0x17
- 0x18
- 0x1A
- 0x1C
- 0x1E
- 0x21
- 0x23
- 0x25
- 0x27
- 0x28
- 0x2A
- 0x2C
- 0x2E
- 0x31
- 0x33
- 0x35
- 0x37
- 0x38
- 0x3A
- 0x3C
- 0x3E
positions:
- 0x01
- 0x03
- 0x05
- 0x07
- 0x08
- 0x0A
- 0x0C
- 0x0E
- 0x11
- 0x13
- 0x15
- 0x17
- 0x18
- 0x1A
- 0x1C
- 0x1E
- 0x21
- 0x23
- 0x25
- 0x27
- 0x28
- 0x2A
- 0x2C
- 0x2E
- 0x31
- 0x33
- 0x35
- 0x37
- 0x38
- 0x3A
- 0x3C
- 0x3E
world: both

View File

@@ -1,3 +1,6 @@
settings:
1:
ow_layout: wild
ow-edges: ow-edges:
1: 1:
groups: groups:

View File

@@ -1,3 +1,6 @@
settings:
1:
ow_layout: wild
ow-edges: ow-edges:
1: 1:
groups: groups:

View File

@@ -0,0 +1,147 @@
settings:
1:
ow_layout: grid
ow-grid:
1:
restricted_positions:
- cells:
- 0x00
- 0x01
- 0x02
- 0x03
- 0x08
- 0x09
- 0x0A
- 0x0B
- 0x10
- 0x11
- 0x12
- 0x13
- 0x18
- 0x19
- 0x1A
- 0x1B
positions:
- 0x00
- 0x01
- 0x02
- 0x03
- 0x08
- 0x09
- 0x0A
- 0x0B
- 0x10
- 0x11
- 0x12
- 0x13
- 0x18
- 0x19
- 0x1A
- 0x1B
world: both
- cells:
- 0x20
- 0x21
- 0x22
- 0x23
- 0x28
- 0x29
- 0x2A
- 0x2B
- 0x30
- 0x31
- 0x32
- 0x33
- 0x38
- 0x39
- 0x3A
- 0x3B
positions:
- 0x20
- 0x21
- 0x22
- 0x23
- 0x28
- 0x29
- 0x2A
- 0x2B
- 0x30
- 0x31
- 0x32
- 0x33
- 0x38
- 0x39
- 0x3A
- 0x3B
world: both
- cells:
- 0x04
- 0x05
- 0x06
- 0x07
- 0x0C
- 0x0D
- 0x0E
- 0x0F
- 0x14
- 0x15
- 0x16
- 0x17
- 0x1C
- 0x1D
- 0x1E
- 0x1F
positions:
- 0x04
- 0x05
- 0x06
- 0x07
- 0x0C
- 0x0D
- 0x0E
- 0x0F
- 0x14
- 0x15
- 0x16
- 0x17
- 0x1C
- 0x1D
- 0x1E
- 0x1F
world: both
- cells:
- 0x24
- 0x25
- 0x26
- 0x27
- 0x2C
- 0x2D
- 0x2E
- 0x2F
- 0x34
- 0x35
- 0x36
- 0x37
- 0x3C
- 0x3D
- 0x3E
- 0x3F
positions:
- 0x24
- 0x25
- 0x26
- 0x27
- 0x2C
- 0x2D
- 0x2E
- 0x2F
- 0x34
- 0x35
- 0x36
- 0x37
- 0x3C
- 0x3D
- 0x3E
- 0x3F
world: both
split_large_screens: true

View File

@@ -1,5 +1,6 @@
settings: settings:
1: 1:
ow_layout: wild
ow_whirlpool: false ow_whirlpool: false
ow-edges: ow-edges:
1: 1:

View File

@@ -1,3 +1,6 @@
settings:
1:
ow_layout: wild
ow-edges: ow-edges:
1: 1:
two-way: two-way:

View File

@@ -1,3 +1,6 @@
settings:
1:
ow_layout: wild
ow-edges: ow-edges:
1: 1:
groups: groups:

View File

@@ -0,0 +1,147 @@
settings:
1:
ow_layout: grid
ow-grid:
1:
restricted_positions:
- cells:
- 0x00
- 0x01
- 0x02
- 0x03
- 0x04
- 0x05
- 0x06
- 0x07
- 0x08
- 0x0F
- 0x10
- 0x17
- 0x18
- 0x1F
- 0x20
- 0x27
- 0x28
- 0x2F
- 0x30
- 0x37
- 0x38
- 0x39
- 0x3A
- 0x3B
- 0x3C
- 0x3D
- 0x3E
- 0x3F
positions:
- 0x00
- 0x01
- 0x02
- 0x03
- 0x04
- 0x05
- 0x06
- 0x07
- 0x08
- 0x0F
- 0x10
- 0x17
- 0x18
- 0x1F
- 0x20
- 0x27
- 0x28
- 0x2F
- 0x30
- 0x37
- 0x38
- 0x39
- 0x3A
- 0x3B
- 0x3C
- 0x3D
- 0x3E
- 0x3F
world: both
- cells:
- 0x09
- 0x0A
- 0x0B
- 0x0C
- 0x0D
- 0x0E
- 0x11
- 0x16
- 0x19
- 0x1E
- 0x21
- 0x26
- 0x29
- 0x2E
- 0x31
- 0x32
- 0x33
- 0x34
- 0x35
- 0x36
positions:
- 0x09
- 0x0A
- 0x0B
- 0x0C
- 0x0D
- 0x0E
- 0x11
- 0x16
- 0x19
- 0x1E
- 0x21
- 0x26
- 0x29
- 0x2E
- 0x31
- 0x32
- 0x33
- 0x34
- 0x35
- 0x36
world: both
- cells:
- 0x12
- 0x13
- 0x14
- 0x15
- 0x1A
- 0x1D
- 0x22
- 0x25
- 0x2A
- 0x2B
- 0x2C
- 0x2D
positions:
- 0x12
- 0x13
- 0x14
- 0x15
- 0x1A
- 0x1D
- 0x22
- 0x25
- 0x2A
- 0x2B
- 0x2C
- 0x2D
world: both
- cells:
- 0x1B
- 0x1C
- 0x23
- 0x24
positions:
- 0x1B
- 0x1C
- 0x23
- 0x24
world: both
split_large_screens: true

View File

@@ -1,3 +1,6 @@
settings:
1:
ow_layout: wild
ow-edges: ow-edges:
1: 1:
two-way: two-way:

View File

@@ -1,5 +1,6 @@
settings: settings:
1: 1:
ow_parallel: false
ow_whirlpool: false ow_whirlpool: false
ow-edges: ow-edges:
1: 1:
@@ -77,6 +78,25 @@ ow-edges:
Desert Pass EC*: Dam WC* Desert Pass EC*: Dam WC*
Desert Pass ES*: Dam WS* Desert Pass ES*: Dam WS*
Dam EC*: South Pass WC* Dam EC*: South Pass WC*
ow-grid:
1:
fixed_arrangements:
- arrangement:
- 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07
- 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F
- 0x10 0x11 0x12 0x13 0x14 0x15 0x16 0x17
- 0x18 0x19 0x1A 0x1B 0x1C 0x1D 0x1E 0x1F
- 0x20 0x21 0x22 0x23 0x24 0x25 0x26 0x27
- 0x28 0x29 0x2A 0x2B 0x2C 0x2D 0x2E 0x2F
- 0x30 0x31 0x32 0x33 0x34 0x35 0x36 0x37
- 0x38 0x39 0x3A 0x3B 0x3C 0x3D 0x3E 0x3F
world: light
restricted_positions:
- cells:
- 0x00
positions:
- 0x00
world: light
ow-whirlpools: ow-whirlpools:
1: 1:
two-way: two-way:

View File

@@ -1,3 +1,6 @@
settings:
1:
ow_layout: wild
ow-edges: ow-edges:
1: 1:
two-way: two-way:

View File

@@ -1,3 +1,6 @@
settings:
1:
ow_layout: wild
ow-edges: ow-edges:
1: 1:
two-way: two-way:

View File

@@ -0,0 +1,149 @@
settings:
1:
ow_layout: grid
ow-grid:
1:
restricted_positions:
- cells:
- 0x00
- 0x03
- 0x05
- 0x18
- 0x1B
- 0x1E
- 0x30
- 0x35
positions:
- 0x00
- 0x03
- 0x05
- 0x18
- 0x1B
- 0x1E
- 0x30
- 0x35
world: both
- cells:
- 0x01
- 0x04
- 0x06
- 0x19
- 0x1C
- 0x1F
- 0x31
- 0x36
positions:
- 0x01
- 0x04
- 0x06
- 0x19
- 0x1C
- 0x1F
- 0x31
- 0x36
world: both
- cells:
- 0x08
- 0x0B
- 0x0D
- 0x20
- 0x23
- 0x26
- 0x38
- 0x3D
positions:
- 0x08
- 0x0B
- 0x0D
- 0x20
- 0x23
- 0x26
- 0x38
- 0x3D
world: both
- cells:
- 0x09
- 0x0C
- 0x0E
- 0x21
- 0x24
- 0x27
- 0x39
- 0x3E
positions:
- 0x09
- 0x0C
- 0x0E
- 0x21
- 0x24
- 0x27
- 0x39
- 0x3E
world: both
- cells:
- 0x02
- 0x07
- 0x0A
- 0x0F
- 0x10
- 0x11
- 0x12
- 0x13
- 0x14
- 0x15
- 0x16
- 0x17
- 0x1A
- 0x1D
- 0x22
- 0x25
- 0x28
- 0x29
- 0x2A
- 0x2B
- 0x2C
- 0x2D
- 0x2E
- 0x2F
- 0x32
- 0x33
- 0x34
- 0x37
- 0x3A
- 0x3B
- 0x3C
- 0x3F
positions:
- 0x02
- 0x07
- 0x0A
- 0x0F
- 0x10
- 0x11
- 0x12
- 0x13
- 0x14
- 0x15
- 0x16
- 0x17
- 0x1A
- 0x1D
- 0x22
- 0x25
- 0x28
- 0x29
- 0x2A
- 0x2B
- 0x2C
- 0x2D
- 0x2E
- 0x2F
- 0x32
- 0x33
- 0x34
- 0x37
- 0x3A
- 0x3B
- 0x3C
- 0x3F
world: both

View File

@@ -1,3 +1,6 @@
settings:
1:
ow_layout: wild
settings: settings:
1: 1:
ow_whirlpool: false ow_whirlpool: false

View File

@@ -1,3 +1,6 @@
settings:
1:
ow_parallel: false
ow-edges: ow-edges:
1: 1:
two-way: two-way:
@@ -69,6 +72,25 @@ ow-edges:
Swamp Nook EC*: Swamp WC* Swamp Nook EC*: Swamp WC*
Swamp Nook ES*: Swamp WS* Swamp Nook ES*: Swamp WS*
Swamp EC*: Dark South Pass WC* Swamp EC*: Dark South Pass WC*
ow-grid:
1:
fixed_arrangements:
- arrangement:
- 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07
- 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F
- 0x10 0x11 0x12 0x13 0x14 0x15 0x16 0x17
- 0x18 0x19 0x1A 0x1B 0x1C 0x1D 0x1E 0x1F
- 0x20 0x21 0x22 0x23 0x24 0x25 0x26 0x27
- 0x28 0x29 0x2A 0x2B 0x2C 0x2D 0x2E 0x2F
- 0x30 0x31 0x32 0x33 0x34 0x35 0x36 0x37
- 0x38 0x39 0x3A 0x3B 0x3C 0x3D 0x3E 0x3F
world: dark
restricted_positions:
- cells:
- 0x00
positions:
- 0x00
world: dark
ow-whirlpools: ow-whirlpools:
1: 1:
two-way: two-way:

View File

@@ -1,3 +1,6 @@
settings:
1:
ow_layout: wild
ow-edges: ow-edges:
1: 1:
groups: groups:

View File

@@ -1,3 +1,6 @@
settings:
1:
ow_layout: wild
ow-edges: ow-edges:
1: 1:
two-way: two-way:

View File

@@ -1,5 +1,6 @@
settings: settings:
1: 1:
ow_layout: wild
ow_terrain: false ow_terrain: false
ow-edges: ow-edges:
1: 1:

View File

@@ -1,5 +1,6 @@
settings: settings:
1: 1:
ow_layout: wild
ow_terrain: true ow_terrain: true
ow-edges: ow-edges:
1: 1:

View File

@@ -1,3 +1,6 @@
settings:
1:
ow_layout: wild
ow-edges: ow-edges:
1: 1:
two-way: two-way:

View File

@@ -0,0 +1,16 @@
ow-edges:
1:
two-way:
West Death Mountain EN*: East Death Mountain WN*
West Death Mountain ES*: East Death Mountain WS*
East Death Mountain EN*: Death Mountain TR Pegs WN*
West Dark Death Mountain EN*: East Dark Death Mountain WN*
West Dark Death Mountain ES*: East Dark Death Mountain WS*
East Dark Death Mountain EN*: Turtle Rock WN*
ow-grid:
1:
fixed_arrangements:
- arrangement:
- 0x03 0x04 0x05 0x06 0x07
- 0x0B 0x0C 0x0D 0x0E .
world: both

View File

@@ -1,3 +1,6 @@
settings:
1:
ow_layout: wild
ow-edges: ow-edges:
1: 1:
two-way: two-way:

View File

@@ -1,3 +1,6 @@
settings:
1:
ow_layout: wild
ow-edges: ow-edges:
1: 1:
two-way: two-way:

View File

@@ -0,0 +1,7 @@
settings:
1:
ow_layout: grid
ow-grid:
1:
wrap_horizontal: true
wrap_vertical: true

View File

@@ -144,6 +144,25 @@ ow-edges:
Swamp EC*: Dark South Pass WC* Swamp EC*: Dark South Pass WC*
South Pass ES*: Lake Hylia WS* South Pass ES*: Lake Hylia WS*
Dark South Pass ES*: Ice Lake WS* Dark South Pass ES*: Ice Lake WS*
ow-grid:
1:
fixed_arrangements:
- arrangement:
- 0x00 0x01 0x02 0x03 0x04 0x05 0x06 0x07
- 0x08 0x09 0x0A 0x0B 0x0C 0x0D 0x0E 0x0F
- 0x10 0x11 0x12 0x13 0x14 0x15 0x16 0x17
- 0x18 0x19 0x1A 0x1B 0x1C 0x1D 0x1E 0x1F
- 0x20 0x21 0x22 0x23 0x24 0x25 0x26 0x27
- 0x28 0x29 0x2A 0x2B 0x2C 0x2D 0x2E 0x2F
- 0x30 0x31 0x32 0x33 0x34 0x35 0x36 0x37
- 0x38 0x39 0x3A 0x3B 0x3C 0x3D 0x3E 0x3F
world: both
restricted_positions:
- cells:
- 0x00
positions:
- 0x00
world: both
ow-whirlpools: ow-whirlpools:
1: 1:
two-way: two-way:

View File

@@ -509,6 +509,7 @@ class CustomSettings(object):
self.world_rep['ow-whirlpools'] = whirlpools = {} self.world_rep['ow-whirlpools'] = whirlpools = {}
self.world_rep['ow-tileflips'] = flips = {} self.world_rep['ow-tileflips'] = flips = {}
self.world_rep['ow-flutespots'] = flute = {} self.world_rep['ow-flutespots'] = flute = {}
self.world_rep['ow-grid'] = owgrid = {}
for p in self.player_range: for p in self.player_range:
connections = edges[p] = {} connections = edges[p] = {}
connections['two-way'] = {} connections['two-way'] = {}
@@ -529,6 +530,57 @@ class CustomSettings(object):
else: else:
flute[p]['force'] = list(HexInt(id) for id in sorted(default_flute_connections)) flute[p]['force'] = list(HexInt(id) for id in sorted(default_flute_connections))
flute[p]['forbid'] = [] flute[p]['forbid'] = []
# layout grid
owgrid[p] = {}
grid = world.owgrid[p]
if grid is None:
grid = [
[[HexInt(row * 8 + col) for col in range(8)] for row in range(8)],
[[HexInt(row * 8 + col) for col in range(8)] for row in range(8)]
]
else:
grid = [
[[HexInt(cell & 0xBF) for cell in row] for row in grid[0]],
[[HexInt(cell & 0xBF) for cell in row] for row in grid[1]]
]
# Create fixed_arrangements for both worlds
owgrid[p]['fixed_arrangements'] = [
{
'arrangement': [' '.join(f'0x{cell:02X}' for cell in row) for row in grid[0]],
'world': 'light'
},
{
'arrangement': [' '.join(f'0x{cell:02X}' for cell in row) for row in grid[1]],
'world': 'dark'
}
]
# Pin top left corners to position 0x00
owgrid[p]['restricted_positions'] = [
{
'cells': [HexInt(grid[0][0][0])],
'positions': [HexInt(0x00)],
'world': 'light'
},
{
'cells': [HexInt(grid[1][0][0])],
'positions': [HexInt(0x00)],
'world': 'dark'
}
]
# Set advanced grid options
horizontal_wrap = False
vertical_wrap = False
split_large_screens = False
if world.customizer:
grid_options = world.customizer.get_owgrid()
if grid_options and p in grid_options:
grid_options = grid_options[p]
horizontal_wrap = 'wrap_horizontal' in grid_options and grid_options['wrap_horizontal'] == True
vertical_wrap = 'wrap_vertical' in grid_options and grid_options['wrap_vertical'] == True
split_large_screens = 'split_large_screens' in grid_options and grid_options['split_large_screens'] == True
owgrid[p]['wrap_horizontal'] = horizontal_wrap
owgrid[p]['wrap_vertical'] = vertical_wrap
owgrid[p]['split_large_screens'] = split_large_screens
for key, data in world.spoiler.overworlds.items(): for key, data in world.spoiler.overworlds.items():
player = data['player'] if 'player' in data else 1 player = data['player'] if 'player' in data else 1
connections = edges[player] connections = edges[player]
@@ -536,7 +588,7 @@ class CustomSettings(object):
connections[sub][data['entrance']] = data['exit'] connections[sub][data['entrance']] = data['exit']
for key, data in world.spoiler.whirlpools.items(): for key, data in world.spoiler.whirlpools.items():
player = data['player'] if 'player' in data else 1 player = data['player'] if 'player' in data else 1
whirlconnects = whirlconnects[player] whirlconnects = whirlpools[player]
sub = 'two-way' if data['direction'] == 'both' else 'one-way' sub = 'two-way' if data['direction'] == 'both' else 'one-way'
whirlconnects[sub][data['entrance']] = data['exit'] whirlconnects[sub][data['entrance']] = data['exit']

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,18 @@ from datetime import datetime
from typing import Dict, List from typing import Dict, List
from PIL import Image, ImageDraw from PIL import Image, ImageDraw
from BaseClasses import Direction, OWEdge from BaseClasses import Direction, OWEdge
from source.overworld.LayoutGenerator import Screen from source.overworld.LayoutGenerator import Screen, get_screen_id_from_cell
def get_quadrant_from_cell_id(cell_id: int, screen_id: int) -> str:
offset = (cell_id & 0xBF) - (screen_id & 0xBF)
if offset == 0x00:
return "NW"
elif offset == 0x01:
return "NE"
elif offset == 0x08:
return "SW"
else:
return "SE"
def get_edge_lists(grid: List[List[List[int]]], def get_edge_lists(grid: List[List[List[int]]],
overworld_screens: Dict[int, Screen], overworld_screens: Dict[int, Screen],
@@ -13,7 +24,7 @@ def get_edge_lists(grid: List[List[List[int]]],
Get list of edges for each cell and direction. Get list of edges for each cell and direction.
Args: Args:
grid: 3D list [world][row][col] containing screen IDs grid: 3D list [world][row][col] containing cell IDs
overworld_screens: Dict of screen_id -> Screen objects overworld_screens: Dict of screen_id -> Screen objects
large_screen_quadrant_info: Dict of screen_id -> quadrant info for large screens large_screen_quadrant_info: Dict of screen_id -> quadrant info for large screens
@@ -24,47 +35,27 @@ def get_edge_lists(grid: List[List[List[int]]],
GRID_SIZE = 8 GRID_SIZE = 8
edge_lists = {} edge_lists = {}
# Large screen base IDs
large_screen_base_ids = [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35,
0x40, 0x43, 0x45, 0x58, 0x5B, 0x5E, 0x70, 0x75]
for world_idx in range(2): for world_idx in range(2):
# Build a map of screen_id -> list of (row, col) positions for large screens
large_screen_positions = {}
for row in range(GRID_SIZE): for row in range(GRID_SIZE):
for col in range(GRID_SIZE): for col in range(GRID_SIZE):
screen_id = grid[world_idx][row][col] cell_id = grid[world_idx][row][col]
if screen_id != -1 and screen_id in large_screen_base_ids:
if screen_id not in large_screen_positions:
large_screen_positions[screen_id] = []
large_screen_positions[screen_id].append((row, col))
for row in range(GRID_SIZE): if cell_id == -1:
for col in range(GRID_SIZE):
screen_id = grid[world_idx][row][col]
if screen_id == -1:
# Empty cell - no edges # Empty cell - no edges
for direction in [Direction.North, Direction.South, Direction.East, Direction.West]: for direction in [Direction.North, Direction.South, Direction.East, Direction.West]:
edge_lists[(world_idx, row, col, direction)] = [] edge_lists[(world_idx, row, col, direction)] = []
continue continue
screen_id = get_screen_id_from_cell(cell_id)
screen = overworld_screens.get(screen_id) screen = overworld_screens.get(screen_id)
if not screen: if not screen:
for direction in [Direction.North, Direction.South, Direction.East, Direction.West]: for direction in [Direction.North, Direction.South, Direction.East, Direction.West]:
edge_lists[(world_idx, row, col, direction)] = [] edge_lists[(world_idx, row, col, direction)] = []
continue continue
is_large = screen_id in large_screen_base_ids if screen.big:
# For large screens, determine quadrant from cell ID
if is_large: quadrant = get_quadrant_from_cell_id(cell_id, screen_id)
# For large screens, determine which quadrant this cell is
# Find all positions of this large screen and determine quadrant
positions = large_screen_positions.get(screen_id, [(row, col)])
# Determine quadrant by finding relative position
# The quadrant is determined by which cells are adjacent
quadrant = determine_large_screen_quadrant(row, col, positions, GRID_SIZE)
# Get edges for this quadrant # Get edges for this quadrant
if screen_id in large_screen_quadrant_info: if screen_id in large_screen_quadrant_info:
@@ -85,45 +76,6 @@ def get_edge_lists(grid: List[List[List[int]]],
return edge_lists return edge_lists
def determine_large_screen_quadrant(row: int, col: int, positions: List[tuple], grid_size: int) -> str:
"""
Determine which quadrant (NW, NE, SW, SE) a cell is in for a large screen.
Handles wrapping correctly by checking adjacency patterns.
Args:
row: Current cell row
col: Current cell column
positions: List of all (row, col) positions for this large screen
grid_size: Size of the grid (8)
Returns:
Quadrant string: "NW", "NE", "SW", or "SE"
"""
positions_set = set(positions)
# Check which adjacent cells also belong to this large screen
has_right = ((row, (col + 1) % grid_size) in positions_set)
has_below = (((row + 1) % grid_size, col) in positions_set)
has_left = ((row, (col - 1) % grid_size) in positions_set)
has_above = (((row - 1) % grid_size, col) in positions_set)
# Determine quadrant based on adjacency
# NW: has right and below neighbors
# NE: has left and below neighbors
# SW: has right and above neighbors
# SE: has left and above neighbors
if has_right and has_below:
return "NW"
elif has_left and has_below:
return "NE"
elif has_right and has_above:
return "SW"
elif has_left and has_above:
return "SE"
else:
raise Exception("?")
def is_crossed_edge(edge: OWEdge, overworld_screens: Dict[int, Screen]) -> bool: def is_crossed_edge(edge: OWEdge, overworld_screens: Dict[int, Screen]) -> bool:
if edge.dest is None: if edge.dest is None:
return False return False
@@ -132,6 +84,40 @@ def is_crossed_edge(edge: OWEdge, overworld_screens: Dict[int, Screen]) -> bool:
dest_screen = overworld_screens.get(edge.dest.owIndex) dest_screen = overworld_screens.get(edge.dest.owIndex)
return source_screen.dark_world != dest_screen.dark_world return source_screen.dark_world != dest_screen.dark_world
def are_large_screen_cells_connected(cell_id1: int, cell_id2: int, quadrant1: str, quadrant2: str, direction: str) -> bool:
"""
Check if two cells of a large screen are connected (should have no border between them).
For cells to be connected:
1. They must be from the same large screen (same base screen ID)
2. Their quadrants must be adjacent in the expected direction
Args:
cell_id1: Cell ID of the first cell
cell_id2: Cell ID of the second cell
quadrant1: Quadrant of the first cell ("NW", "NE", "SW", "SE")
quadrant2: Quadrant of the second cell
direction: Direction from cell1 to cell2 ("east", "south")
Returns:
True if the cells should have no border between them
"""
# Must be from the same large screen
screen_id1 = get_screen_id_from_cell(cell_id1)
screen_id2 = get_screen_id_from_cell(cell_id2)
if screen_id1 != screen_id2:
return False
# Check if quadrants are properly adjacent
if direction == "east":
# For east connection: NW->NE or SW->SE
return (quadrant1 == "NW" and quadrant2 == "NE") or (quadrant1 == "SW" and quadrant2 == "SE")
elif direction == "south":
# For south connection: NW->SW or NE->SE
return (quadrant1 == "NW" and quadrant2 == "SW") or (quadrant1 == "NE" and quadrant2 == "SE")
return False
def visualize_layout(grid: List[List[List[int]]], output_dir: str, def visualize_layout(grid: List[List[List[int]]], output_dir: str,
overworld_screens: Dict[int, Screen], overworld_screens: Dict[int, Screen],
large_screen_quadrant_info: Dict[int, Dict]) -> None: large_screen_quadrant_info: Dict[int, Dict]) -> None:
@@ -162,78 +148,42 @@ def visualize_layout(grid: List[List[List[int]]], output_dir: str,
output_height = world_height output_height = world_height
output_img = Image.new('RGB', (output_width, output_height), color='black') output_img = Image.new('RGB', (output_width, output_height), color='black')
# Large screen base IDs (defined once for reuse)
large_screen_base_ids = [0x00, 0x03, 0x05, 0x18, 0x1B, 0x1E, 0x30, 0x35,
0x40, 0x43, 0x45, 0x58, 0x5B, 0x5E, 0x70, 0x75]
# Process both worlds # Process both worlds
for world_idx in range(2): for world_idx in range(2):
x_offset = 0 if world_idx == 0 else (world_width + gap) x_offset = 0 if world_idx == 0 else (world_width + gap)
# Build a map of screen_id -> list of (row, col) positions for large screens
large_screen_positions = {}
for row in range(GRID_SIZE):
for col in range(GRID_SIZE):
screen_id = grid[world_idx][row][col]
if screen_id != -1 and screen_id in large_screen_base_ids:
if screen_id not in large_screen_positions:
large_screen_positions[screen_id] = []
large_screen_positions[screen_id].append((row, col))
# Process each cell in the grid individually # Process each cell in the grid individually
# This handles wrapped large screens correctly by drawing each quadrant separately
for row in range(GRID_SIZE): for row in range(GRID_SIZE):
for col in range(GRID_SIZE): for col in range(GRID_SIZE):
screen_id = grid[world_idx][row][col] cell_id = grid[world_idx][row][col]
if screen_id == -1: if cell_id == -1:
# Empty cell - fill with black (already black from initialization) # Empty cell - fill with black (already black from initialization)
continue continue
is_large = screen_id in large_screen_base_ids screen_id = get_screen_id_from_cell(cell_id)
screen = overworld_screens.get(screen_id)
if not screen:
continue
# Calculate source position in the world image is_large = screen.big
source_row = (screen_id % 0x40) >> 3
source_col = screen_id % 0x08
world_img = lightworld_img if screen_id < 0x40 else darkworld_img
if is_large: # Calculate source position in the world image based on cell_id
# For large screens, determine which quadrant this cell represents # For large screens, cell_id already encodes the quadrant position
positions = large_screen_positions.get(screen_id, [(row, col)]) source_row = (cell_id % 0x40) >> 3
quadrant = determine_large_screen_quadrant(row, col, positions, GRID_SIZE) source_col = cell_id % 0x08
world_img = lightworld_img if cell_id < 0x40 else darkworld_img
# Map quadrant to source offset within the 2x2 large screen source_x = source_col * SOURCE_CELL_SIZE
quadrant_offsets = { source_y = source_row * SOURCE_CELL_SIZE
"NW": (0, 0),
"NE": (1, 0),
"SW": (0, 1),
"SE": (1, 1)
}
q_col_offset, q_row_offset = quadrant_offsets[quadrant]
# Calculate source position for this quadrant # Crop single cell from source
source_x = (source_col + q_col_offset) * SOURCE_CELL_SIZE cropped = world_img.crop((
source_y = (source_row + q_row_offset) * SOURCE_CELL_SIZE source_x,
source_y,
# Crop single cell from source (the specific quadrant) source_x + SOURCE_CELL_SIZE,
cropped = world_img.crop(( source_y + SOURCE_CELL_SIZE
source_x, ))
source_y,
source_x + SOURCE_CELL_SIZE,
source_y + SOURCE_CELL_SIZE
))
else:
# Small screen (1x1)
source_x = source_col * SOURCE_CELL_SIZE
source_y = source_row * SOURCE_CELL_SIZE
# Crop single cell from source
cropped = world_img.crop((
source_x,
source_y,
source_x + SOURCE_CELL_SIZE,
source_y + SOURCE_CELL_SIZE
))
# Resize to output size (64x64 pixels) # Resize to output size (64x64 pixels)
resized = cropped.resize( resized = cropped.resize(
@@ -257,52 +207,93 @@ def visualize_layout(grid: List[List[List[int]]], output_dir: str,
for world_idx in range(2): for world_idx in range(2):
x_offset = 0 if world_idx == 0 else (world_width + gap) x_offset = 0 if world_idx == 0 else (world_width + gap)
# Build large screen positions map for this world
large_screen_positions = {}
for row in range(GRID_SIZE):
for col in range(GRID_SIZE):
screen_id = grid[world_idx][row][col]
if screen_id != -1 and screen_id in large_screen_base_ids:
if screen_id not in large_screen_positions:
large_screen_positions[screen_id] = []
large_screen_positions[screen_id].append((row, col))
# Draw borders for each cell # Draw borders for each cell
# For large screens, only draw borders where cells are not connected
for row in range(GRID_SIZE): for row in range(GRID_SIZE):
for col in range(GRID_SIZE): for col in range(GRID_SIZE):
screen_id = grid[world_idx][row][col] cell_id = grid[world_idx][row][col]
if screen_id == -1: if cell_id == -1:
continue continue
is_large = screen_id in large_screen_base_ids screen_id = get_screen_id_from_cell(cell_id)
screen = overworld_screens.get(screen_id)
if not screen:
continue
is_large = screen.big
dest_x = x_offset + col * OUTPUT_CELL_SIZE dest_x = x_offset + col * OUTPUT_CELL_SIZE
dest_y = row * OUTPUT_CELL_SIZE dest_y = row * OUTPUT_CELL_SIZE
if is_large: if is_large:
# For large screens, determine which quadrant this cell is quadrant = get_quadrant_from_cell_id(cell_id, screen_id)
positions = large_screen_positions.get(screen_id, [(row, col)])
quadrant = determine_large_screen_quadrant(row, col, positions, GRID_SIZE)
# Draw border only on the outer edges of the large screen # Check each border direction
# (not on internal edges between quadrants) # Top border: draw if this is a north quadrant OR if the cell above is not connected
# NW: draw top and left borders draw_top = True
# NE: draw top and right borders
# SW: draw bottom and left borders
# SE: draw bottom and right borders
if quadrant in ["NW", "NE"]:
# Draw top border
draw.line([(dest_x, dest_y), (dest_x + OUTPUT_CELL_SIZE - 1, dest_y)], fill='black', width=BORDER_WIDTH)
if quadrant in ["SW", "SE"]: if quadrant in ["SW", "SE"]:
# Draw bottom border # Check if cell above is connected
draw.line([(dest_x, dest_y + OUTPUT_CELL_SIZE - 1), (dest_x + OUTPUT_CELL_SIZE - 1, dest_y + OUTPUT_CELL_SIZE - 1)], fill='black', width=BORDER_WIDTH) above_row = (row - 1) % GRID_SIZE
if quadrant in ["NW", "SW"]: above_cell_id = grid[world_idx][above_row][col]
# Draw left border if above_cell_id != -1:
draw.line([(dest_x, dest_y), (dest_x, dest_y + OUTPUT_CELL_SIZE - 1)], fill='black', width=BORDER_WIDTH) above_screen_id = get_screen_id_from_cell(above_cell_id)
above_screen = overworld_screens.get(above_screen_id)
if above_screen and above_screen.big:
above_quadrant = get_quadrant_from_cell_id(above_cell_id, above_screen_id)
if are_large_screen_cells_connected(above_cell_id, cell_id, above_quadrant, quadrant, "south"):
draw_top = False
# Bottom border: draw if this is a south quadrant OR if the cell below is not connected
draw_bottom = True
if quadrant in ["NW", "NE"]:
# Check if cell below is connected
below_row = (row + 1) % GRID_SIZE
below_cell_id = grid[world_idx][below_row][col]
if below_cell_id != -1:
below_screen_id = get_screen_id_from_cell(below_cell_id)
below_screen = overworld_screens.get(below_screen_id)
if below_screen and below_screen.big:
below_quadrant = get_quadrant_from_cell_id(below_cell_id, below_screen_id)
if are_large_screen_cells_connected(cell_id, below_cell_id, quadrant, below_quadrant, "south"):
draw_bottom = False
# Left border: draw if this is a west quadrant OR if the cell to the left is not connected
draw_left = True
if quadrant in ["NE", "SE"]: if quadrant in ["NE", "SE"]:
# Draw right border # Check if cell to the left is connected
left_col = (col - 1) % GRID_SIZE
left_cell_id = grid[world_idx][row][left_col]
if left_cell_id != -1:
left_screen_id = get_screen_id_from_cell(left_cell_id)
left_screen = overworld_screens.get(left_screen_id)
if left_screen and left_screen.big:
left_quadrant = get_quadrant_from_cell_id(left_cell_id, left_screen_id)
if are_large_screen_cells_connected(left_cell_id, cell_id, left_quadrant, quadrant, "east"):
draw_left = False
# Right border: draw if this is an east quadrant OR if the cell to the right is not connected
draw_right = True
if quadrant in ["NW", "SW"]:
# Check if cell to the right is connected
right_col = (col + 1) % GRID_SIZE
right_cell_id = grid[world_idx][row][right_col]
if right_cell_id != -1:
right_screen_id = get_screen_id_from_cell(right_cell_id)
right_screen = overworld_screens.get(right_screen_id)
if right_screen and right_screen.big:
right_quadrant = get_quadrant_from_cell_id(right_cell_id, right_screen_id)
if are_large_screen_cells_connected(cell_id, right_cell_id, quadrant, right_quadrant, "east"):
draw_right = False
# Draw the borders
if draw_top:
draw.line([(dest_x, dest_y), (dest_x + OUTPUT_CELL_SIZE - 1, dest_y)], fill='black', width=BORDER_WIDTH)
if draw_bottom:
draw.line([(dest_x, dest_y + OUTPUT_CELL_SIZE - 1), (dest_x + OUTPUT_CELL_SIZE - 1, dest_y + OUTPUT_CELL_SIZE - 1)], fill='black', width=BORDER_WIDTH)
if draw_left:
draw.line([(dest_x, dest_y), (dest_x, dest_y + OUTPUT_CELL_SIZE - 1)], fill='black', width=BORDER_WIDTH)
if draw_right:
draw.line([(dest_x + OUTPUT_CELL_SIZE - 1, dest_y), (dest_x + OUTPUT_CELL_SIZE - 1, dest_y + OUTPUT_CELL_SIZE - 1)], fill='black', width=BORDER_WIDTH) draw.line([(dest_x + OUTPUT_CELL_SIZE - 1, dest_y), (dest_x + OUTPUT_CELL_SIZE - 1, dest_y + OUTPUT_CELL_SIZE - 1)], fill='black', width=BORDER_WIDTH)
else: else:
# Small screen - draw border around single cell # Small screen - draw border around single cell
@@ -315,8 +306,8 @@ def visualize_layout(grid: List[List[List[int]]], output_dir: str,
# Draw edge connection indicators for each cell # Draw edge connection indicators for each cell
for row in range(GRID_SIZE): for row in range(GRID_SIZE):
for col in range(GRID_SIZE): for col in range(GRID_SIZE):
screen_id = grid[world_idx][row][col] cell_id = grid[world_idx][row][col]
if screen_id == -1: if cell_id == -1:
continue continue
dest_x = x_offset + col * OUTPUT_CELL_SIZE dest_x = x_offset + col * OUTPUT_CELL_SIZE