python-library-ff14-the-hunt 0.0.10__tar.gz → 0.0.11__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/PKG-INFO +1 -1
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/bear_tracker/spawn_window.py +19 -7
- python_library_ff14_the_hunt-0.0.11/ff14_the_hunt/common/http_request.py +3 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/locale/cn.py +6 -6
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/locale/en.py +5 -5
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/models.py +15 -15
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/poll/spawn_dedup.py +34 -32
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/spawn_map/attach.py +17 -12
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/pyproject.toml +1 -1
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/tests/test_spawn_dedup.py +172 -73
- python_library_ff14_the_hunt-0.0.11/tests/test_spawn_map_data.py +194 -0
- python_library_ff14_the_hunt-0.0.10/ff14_the_hunt/common/http_request.py +0 -3
- python_library_ff14_the_hunt-0.0.10/tests/test_spawn_map_data.py +0 -74
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/.cursor/skills/ff14_the_hunt-package-changelog/SKILL.md +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/.cursor/skills/ff14_the_hunt-package-design/SKILL.md +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/.cursor/skills/ff14_the_hunt-package-preload/SKILL.md +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/.gitignore +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/README.md +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/example/__main__.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/example/spawn_map_io.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/example.bat +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/__init__.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/bear_tracker/__init__.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/bear_tracker/client.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/bear_tracker/enrich.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/bear_tracker/fate_timer.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/bear_tracker/resources.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/bear_tracker/spawn_points.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/bear_tracker/timer_theme.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/common/__init__.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/common/urlopen_retry.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/ff14_the_hunt.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/locale/__init__.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/locale/data/__init__.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/locale/data/hunts_zh.json +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/locale/data/regions_zh.json +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/locale/detect.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/locale/display.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/locale/names.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/locale/spawn_map_display.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/locale/tag.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/poll/__init__.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/poll/loop.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/poll/monitor_mode.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/poll/scheduler.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/poll/sleep_plan.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/poll/sleep_settings.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/poll/window_remaining.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/spawn_map/__init__.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/spawn_map/coordinates.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/spawn_map/layout.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/spawn_map/png_meta.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/spawn_map/points.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/spawn_map/region_fetch.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/spawn_map/region_image.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/test.bat +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/tests/__init__.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/tests/test_bear_tracker_client.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/tests/test_fate_timer.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/tests/test_ff14_the_hunt_service.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/tests/test_locale_cn.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/tests/test_monitor_mode.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/tests/test_poll_scheduler.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/tests/test_poll_sleep.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/tests/test_recent_spawn_detection.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/tests/test_spawn_map_points.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/tests/test_spawn_window.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/tests/test_untimed_marks_filter.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/tests/test_urlopen_retry.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/tools/build_locale_data.py +0 -0
- {python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/update.bat +0 -0
|
@@ -85,13 +85,25 @@ def compute_trigger_timer(
|
|
|
85
85
|
)
|
|
86
86
|
|
|
87
87
|
|
|
88
|
-
def is_window_open(timer: TimerDisplay | None) -> bool:
|
|
89
|
-
return timer is not None and timer.phase == SpawnWindowPhase.OPEN
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def
|
|
93
|
-
timer
|
|
94
|
-
|
|
88
|
+
def is_window_open(timer: TimerDisplay | None) -> bool:
|
|
89
|
+
return timer is not None and timer.phase == SpawnWindowPhase.OPEN
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def is_window_report_ready(timer: TimerDisplay | None) -> bool:
|
|
93
|
+
if timer is None:
|
|
94
|
+
return False
|
|
95
|
+
if timer.phase == SpawnWindowPhase.CAPPED:
|
|
96
|
+
return True
|
|
97
|
+
if timer.phase != SpawnWindowPhase.OPEN:
|
|
98
|
+
return False
|
|
99
|
+
if timer.progress_percent is None:
|
|
100
|
+
return False
|
|
101
|
+
return timer.progress_percent >= 100.0
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def is_recently_in_window(
|
|
105
|
+
timer: TimerDisplay | None,
|
|
106
|
+
*,
|
|
95
107
|
grace_seconds: float = 900.0,
|
|
96
108
|
) -> bool:
|
|
97
109
|
if timer is None or timer.phase != SpawnWindowPhase.OPEN:
|
|
@@ -121,11 +121,11 @@ def mark_to_display_dict(
|
|
|
121
121
|
"上次死亡时间": mark.last_death_time,
|
|
122
122
|
"上次标记时间": mark.last_mark_time,
|
|
123
123
|
"失踪计数": mark.missing_counter,
|
|
124
|
-
"维护计时": mark.is_maintenance,
|
|
125
|
-
"刚刷新": mark.recently_spawned,
|
|
126
|
-
"
|
|
127
|
-
"新检出": mark.newly_spawned,
|
|
128
|
-
}
|
|
124
|
+
"维护计时": mark.is_maintenance,
|
|
125
|
+
"刚刷新": mark.recently_spawned,
|
|
126
|
+
"开窗100%": mark.newly_opened,
|
|
127
|
+
"新检出": mark.newly_spawned,
|
|
128
|
+
}
|
|
129
129
|
if mark.fate_last_seen is not None:
|
|
130
130
|
item["FATE最近发现"] = mark.fate_last_seen
|
|
131
131
|
if mark.fate_last_death is not None:
|
|
@@ -153,7 +153,7 @@ def mark_to_display_dict(
|
|
|
153
153
|
"像素Y": point.pixel_y,
|
|
154
154
|
"格点X": point.grid_x,
|
|
155
155
|
"格点Y": point.grid_y,
|
|
156
|
-
"可能触发": point.active,
|
|
156
|
+
"可能触发": point.active,
|
|
157
157
|
}
|
|
158
158
|
for point in mark.spawn_points
|
|
159
159
|
]
|
|
@@ -108,11 +108,11 @@ def mark_to_display_dict(
|
|
|
108
108
|
"last_death_time": mark.last_death_time,
|
|
109
109
|
"last_mark_time": mark.last_mark_time,
|
|
110
110
|
"missing_counter": mark.missing_counter,
|
|
111
|
-
"is_maintenance": mark.is_maintenance,
|
|
112
|
-
"recently_spawned": mark.recently_spawned,
|
|
113
|
-
"newly_opened": mark.newly_opened,
|
|
114
|
-
"newly_spawned": mark.newly_spawned,
|
|
115
|
-
}
|
|
111
|
+
"is_maintenance": mark.is_maintenance,
|
|
112
|
+
"recently_spawned": mark.recently_spawned,
|
|
113
|
+
"newly_opened": mark.newly_opened,
|
|
114
|
+
"newly_spawned": mark.newly_spawned,
|
|
115
|
+
}
|
|
116
116
|
if mark.fate_last_seen is not None:
|
|
117
117
|
item["fate_last_seen"] = mark.fate_last_seen
|
|
118
118
|
if mark.fate_last_death is not None:
|
{python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/ff14_the_hunt/models.py
RENAMED
|
@@ -58,7 +58,7 @@ class MapCoordinate(BaseModel):
|
|
|
58
58
|
default=None,
|
|
59
59
|
description="区域原图上的 Y 像素;norm_y * 图高",
|
|
60
60
|
)
|
|
61
|
-
active: bool | None = Field(default=None, description="是否仍是可能触发点位")
|
|
61
|
+
active: bool | None = Field(default=None, description="是否仍是可能触发点位")
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
class SpawnMapLayout(BaseModel):
|
|
@@ -127,7 +127,7 @@ class HuntMarkRecord(BaseModel):
|
|
|
127
127
|
fate_timer: TimerDisplay | None = None
|
|
128
128
|
spawn_points: list[MapCoordinate] = Field(
|
|
129
129
|
default_factory=list,
|
|
130
|
-
description="仅刚刷新记录填充;只保留可能触发点位",
|
|
130
|
+
description="仅刚刷新记录填充;只保留可能触发点位",
|
|
131
131
|
)
|
|
132
132
|
spawn_map_layout: SpawnMapLayout | None = Field(
|
|
133
133
|
default=None,
|
|
@@ -137,15 +137,15 @@ class HuntMarkRecord(BaseModel):
|
|
|
137
137
|
default=None,
|
|
138
138
|
description="仅刚刷新且启用区域原图时填充",
|
|
139
139
|
)
|
|
140
|
-
recently_spawned: bool = False
|
|
140
|
+
recently_spawned: bool = False
|
|
141
141
|
newly_opened: bool = Field(
|
|
142
142
|
default=False,
|
|
143
|
-
description="First reported trigger-window
|
|
144
|
-
)
|
|
145
|
-
newly_spawned: bool = Field(
|
|
146
|
-
default=False,
|
|
147
|
-
description="First report-worthy event in this polling session.",
|
|
143
|
+
description="First reported 100-percent trigger-window event in this polling session.",
|
|
148
144
|
)
|
|
145
|
+
newly_spawned: bool = Field(
|
|
146
|
+
default=False,
|
|
147
|
+
description="First report-worthy event in this polling session.",
|
|
148
|
+
)
|
|
149
149
|
raw_timer: dict[str, Any] = Field(default_factory=dict)
|
|
150
150
|
|
|
151
151
|
|
|
@@ -197,10 +197,10 @@ class HuntCrawlPacket(BaseModel):
|
|
|
197
197
|
marks: list[HuntMarkRecord] = Field(default_factory=list, description="解析后的狩猎记录")
|
|
198
198
|
query: HuntQueryFilter = Field(description="本次爬取使用的筛选条件")
|
|
199
199
|
|
|
200
|
-
@property
|
|
201
|
-
def newly_spawned_marks(self) -> list[HuntMarkRecord]:
|
|
202
|
-
return [mark for mark in self.marks if mark.newly_spawned]
|
|
203
|
-
|
|
204
|
-
@property
|
|
205
|
-
def newly_opened_marks(self) -> list[HuntMarkRecord]:
|
|
206
|
-
return [mark for mark in self.marks if mark.newly_opened]
|
|
200
|
+
@property
|
|
201
|
+
def newly_spawned_marks(self) -> list[HuntMarkRecord]:
|
|
202
|
+
return [mark for mark in self.marks if mark.newly_spawned]
|
|
203
|
+
|
|
204
|
+
@property
|
|
205
|
+
def newly_opened_marks(self) -> list[HuntMarkRecord]:
|
|
206
|
+
return [mark for mark in self.marks if mark.newly_opened]
|
|
@@ -1,41 +1,43 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
from ff14_the_hunt.
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
self.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
mark.
|
|
25
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from ff14_the_hunt.bear_tracker.spawn_window import is_window_report_ready
|
|
4
|
+
from ff14_the_hunt.models import HuntMarkRecord
|
|
5
|
+
|
|
6
|
+
SpawnCycleKey = tuple[str, str, float | None]
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def spawn_cycle_key(mark: HuntMarkRecord) -> SpawnCycleKey:
|
|
10
|
+
"""Return the per-hunt death cycle used for one-shot reports."""
|
|
11
|
+
return (mark.hunt_key, mark.world_name, mark.last_death_time)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class SpawnReportTracker:
|
|
15
|
+
"""Track one-shot report events across polling rounds."""
|
|
16
|
+
|
|
17
|
+
def __init__(self) -> None:
|
|
18
|
+
self._reported_spawn_cycles: set[SpawnCycleKey] = set()
|
|
19
|
+
self._reported_open_cycles: set[SpawnCycleKey] = set()
|
|
20
|
+
|
|
21
|
+
def apply(self, marks: list[HuntMarkRecord]) -> None:
|
|
22
|
+
for mark in marks:
|
|
23
|
+
cycle = spawn_cycle_key(mark)
|
|
24
|
+
mark.newly_opened = self._should_report_open_window(mark, cycle)
|
|
25
|
+
mark.newly_spawned = self._should_report_spawn(mark, cycle) or mark.newly_opened
|
|
26
|
+
|
|
26
27
|
def _should_report_spawn(self, mark: HuntMarkRecord, cycle: SpawnCycleKey) -> bool:
|
|
27
28
|
if not mark.recently_spawned:
|
|
28
29
|
return False
|
|
30
|
+
if not is_window_report_ready(mark.trigger_timer):
|
|
31
|
+
return False
|
|
29
32
|
if cycle in self._reported_spawn_cycles:
|
|
30
33
|
return False
|
|
31
|
-
self._reported_spawn_cycles.add(cycle)
|
|
32
|
-
return True
|
|
33
|
-
|
|
34
|
+
self._reported_spawn_cycles.add(cycle)
|
|
35
|
+
return True
|
|
36
|
+
|
|
34
37
|
def _should_report_open_window(self, mark: HuntMarkRecord, cycle: SpawnCycleKey) -> bool:
|
|
35
|
-
|
|
36
|
-
if trigger is None or trigger.phase != SpawnWindowPhase.OPEN:
|
|
38
|
+
if not is_window_report_ready(mark.trigger_timer):
|
|
37
39
|
return False
|
|
38
40
|
if cycle in self._reported_open_cycles:
|
|
39
41
|
return False
|
|
40
|
-
self._reported_open_cycles.add(cycle)
|
|
41
|
-
return True
|
|
42
|
+
self._reported_open_cycles.add(cycle)
|
|
43
|
+
return True
|
|
@@ -2,7 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import Any
|
|
4
4
|
|
|
5
|
-
from ff14_the_hunt.bear_tracker.resources import BearResources
|
|
5
|
+
from ff14_the_hunt.bear_tracker.resources import BearResources
|
|
6
|
+
from ff14_the_hunt.bear_tracker.spawn_window import is_window_report_ready
|
|
6
7
|
from ff14_the_hunt.bear_tracker.spawn_points import list_map_coordinates
|
|
7
8
|
from ff14_the_hunt.models import HuntMarkRecord
|
|
8
9
|
|
|
@@ -13,25 +14,29 @@ from ff14_the_hunt.spawn_map.region_fetch import RegionMapFetcher
|
|
|
13
14
|
from ff14_the_hunt.spawn_map.region_image import build_region_map_image
|
|
14
15
|
|
|
15
16
|
|
|
16
|
-
def _spawn_entry_for_record(
|
|
17
|
-
record: HuntMarkRecord,
|
|
18
|
-
resources: BearResources,
|
|
19
|
-
) -> dict[str, Any] | None:
|
|
17
|
+
def _spawn_entry_for_record(
|
|
18
|
+
record: HuntMarkRecord,
|
|
19
|
+
resources: BearResources,
|
|
20
|
+
) -> dict[str, Any] | None:
|
|
20
21
|
meta = resources.hunt_meta(record.hunt_key)
|
|
21
22
|
map_key = resources.spawn_map_key(record.hunt_key, meta)
|
|
22
23
|
if not map_key:
|
|
23
24
|
return None
|
|
24
|
-
return resources.spawn_point.get(map_key)
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
def
|
|
25
|
+
return resources.spawn_point.get(map_key)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def should_attach_spawn_details(record: HuntMarkRecord) -> bool:
|
|
29
|
+
return is_window_report_ready(record.trigger_timer)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def populate_spawn_points(
|
|
28
33
|
record: HuntMarkRecord,
|
|
29
34
|
*,
|
|
30
35
|
resources: BearResources,
|
|
31
36
|
spawn_states: dict[str, Any] | None,
|
|
32
37
|
) -> None:
|
|
33
38
|
"""为刚刷新记录填充 ``spawn_points`` 与 ``spawn_map_layout``。"""
|
|
34
|
-
if not record
|
|
39
|
+
if not should_attach_spawn_details(record):
|
|
35
40
|
record.spawn_points = []
|
|
36
41
|
record.spawn_map_layout = None
|
|
37
42
|
record.region_map = None
|
|
@@ -49,7 +54,7 @@ def attach_region_map(
|
|
|
49
54
|
) -> None:
|
|
50
55
|
"""拉取站点区域原图,并补全刷点像素坐标。"""
|
|
51
56
|
record.region_map = None
|
|
52
|
-
if not record
|
|
57
|
+
if not should_attach_spawn_details(record) or not record.spawn_points:
|
|
53
58
|
return
|
|
54
59
|
region = resolve_region_name(record.region)
|
|
55
60
|
if not region:
|
|
@@ -80,7 +85,7 @@ def enrich_recent_spawn_details(
|
|
|
80
85
|
for row in timer_rows
|
|
81
86
|
}
|
|
82
87
|
for record in records:
|
|
83
|
-
if not record
|
|
88
|
+
if not should_attach_spawn_details(record):
|
|
84
89
|
record.spawn_points = []
|
|
85
90
|
record.spawn_map_layout = None
|
|
86
91
|
record.region_map = None
|
|
@@ -1,42 +1,44 @@
|
|
|
1
|
-
from ff14_the_hunt.bear_tracker.timer_theme import build_timer_display
|
|
2
|
-
from ff14_the_hunt.models import (
|
|
3
|
-
HuntCrawlPacket,
|
|
4
|
-
HuntMarkRecord,
|
|
5
|
-
HuntQueryFilter,
|
|
6
|
-
SpawnWindowPhase,
|
|
7
|
-
TimerBarColor,
|
|
8
|
-
TimerKind,
|
|
9
|
-
)
|
|
10
|
-
from ff14_the_hunt.poll.spawn_dedup import SpawnReportTracker, spawn_cycle_key
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
def _mark(
|
|
14
|
-
*,
|
|
15
|
-
hunt_key: str = "Foo",
|
|
16
|
-
world: str = "Bar",
|
|
1
|
+
from ff14_the_hunt.bear_tracker.timer_theme import build_timer_display
|
|
2
|
+
from ff14_the_hunt.models import (
|
|
3
|
+
HuntCrawlPacket,
|
|
4
|
+
HuntMarkRecord,
|
|
5
|
+
HuntQueryFilter,
|
|
6
|
+
SpawnWindowPhase,
|
|
7
|
+
TimerBarColor,
|
|
8
|
+
TimerKind,
|
|
9
|
+
)
|
|
10
|
+
from ff14_the_hunt.poll.spawn_dedup import SpawnReportTracker, spawn_cycle_key
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _mark(
|
|
14
|
+
*,
|
|
15
|
+
hunt_key: str = "Foo",
|
|
16
|
+
world: str = "Bar",
|
|
17
17
|
last_death: float = 1000.0,
|
|
18
18
|
recently: bool = True,
|
|
19
19
|
phase: SpawnWindowPhase | None = None,
|
|
20
|
+
progress_percent: float | None = None,
|
|
20
21
|
) -> HuntMarkRecord:
|
|
21
|
-
trigger_timer = None
|
|
22
|
-
if phase is not None:
|
|
23
|
-
trigger_timer = build_timer_display(
|
|
24
|
-
kind=TimerKind.TRIGGER,
|
|
25
|
-
phase=phase,
|
|
26
|
-
bar_color=TimerBarColor.SUCCESS
|
|
27
|
-
if phase == SpawnWindowPhase.OPEN
|
|
28
|
-
else TimerBarColor.ERROR,
|
|
22
|
+
trigger_timer = None
|
|
23
|
+
if phase is not None:
|
|
24
|
+
trigger_timer = build_timer_display(
|
|
25
|
+
kind=TimerKind.TRIGGER,
|
|
26
|
+
phase=phase,
|
|
27
|
+
bar_color=TimerBarColor.SUCCESS
|
|
28
|
+
if phase == SpawnWindowPhase.OPEN
|
|
29
|
+
else TimerBarColor.ERROR,
|
|
29
30
|
counts_up=phase == SpawnWindowPhase.OPEN,
|
|
31
|
+
progress_percent=progress_percent,
|
|
30
32
|
summary="test",
|
|
31
33
|
)
|
|
32
|
-
return HuntMarkRecord(
|
|
33
|
-
hunt_key=hunt_key,
|
|
34
|
-
hunt_name=hunt_key,
|
|
35
|
-
world_name=world,
|
|
36
|
-
last_death_time=last_death,
|
|
37
|
-
recently_spawned=recently,
|
|
38
|
-
trigger_timer=trigger_timer,
|
|
39
|
-
)
|
|
34
|
+
return HuntMarkRecord(
|
|
35
|
+
hunt_key=hunt_key,
|
|
36
|
+
hunt_name=hunt_key,
|
|
37
|
+
world_name=world,
|
|
38
|
+
last_death_time=last_death,
|
|
39
|
+
recently_spawned=recently,
|
|
40
|
+
trigger_timer=trigger_timer,
|
|
41
|
+
)
|
|
40
42
|
|
|
41
43
|
|
|
42
44
|
def test_spawn_cycle_key_uses_last_death() -> None:
|
|
@@ -44,29 +46,58 @@ def test_spawn_cycle_key_uses_last_death() -> None:
|
|
|
44
46
|
assert spawn_cycle_key(mark) == ("Foo", "Bar", 42.0)
|
|
45
47
|
|
|
46
48
|
|
|
47
|
-
def test_tracker_marks_first_recent_as_new() -> None:
|
|
48
|
-
tracker = SpawnReportTracker()
|
|
49
|
-
marks = [
|
|
50
|
-
|
|
51
|
-
|
|
49
|
+
def test_tracker_marks_first_recent_as_new() -> None:
|
|
50
|
+
tracker = SpawnReportTracker()
|
|
51
|
+
marks = [
|
|
52
|
+
_mark(
|
|
53
|
+
phase=SpawnWindowPhase.OPEN,
|
|
54
|
+
progress_percent=100.0,
|
|
55
|
+
)
|
|
56
|
+
]
|
|
57
|
+
tracker.apply(marks)
|
|
58
|
+
assert marks[0].newly_spawned is True
|
|
52
59
|
|
|
53
60
|
|
|
54
|
-
def test_tracker_skips_repeat_within_same_cycle() -> None:
|
|
55
|
-
tracker = SpawnReportTracker()
|
|
56
|
-
first = [
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
def test_tracker_skips_repeat_within_same_cycle() -> None:
|
|
62
|
+
tracker = SpawnReportTracker()
|
|
63
|
+
first = [
|
|
64
|
+
_mark(
|
|
65
|
+
phase=SpawnWindowPhase.OPEN,
|
|
66
|
+
progress_percent=100.0,
|
|
67
|
+
)
|
|
68
|
+
]
|
|
69
|
+
tracker.apply(first)
|
|
70
|
+
second = [
|
|
71
|
+
_mark(
|
|
72
|
+
phase=SpawnWindowPhase.OPEN,
|
|
73
|
+
progress_percent=100.0,
|
|
74
|
+
)
|
|
75
|
+
]
|
|
76
|
+
tracker.apply(second)
|
|
77
|
+
assert second[0].newly_spawned is False
|
|
78
|
+
assert second[0].recently_spawned is True
|
|
62
79
|
|
|
63
80
|
|
|
64
|
-
def test_tracker_reports_again_after_new_death_cycle() -> None:
|
|
65
|
-
tracker = SpawnReportTracker()
|
|
66
|
-
tracker.apply(
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
81
|
+
def test_tracker_reports_again_after_new_death_cycle() -> None:
|
|
82
|
+
tracker = SpawnReportTracker()
|
|
83
|
+
tracker.apply(
|
|
84
|
+
[
|
|
85
|
+
_mark(
|
|
86
|
+
last_death=10.0,
|
|
87
|
+
phase=SpawnWindowPhase.OPEN,
|
|
88
|
+
progress_percent=100.0,
|
|
89
|
+
)
|
|
90
|
+
]
|
|
91
|
+
)
|
|
92
|
+
later = [
|
|
93
|
+
_mark(
|
|
94
|
+
last_death=20.0,
|
|
95
|
+
phase=SpawnWindowPhase.OPEN,
|
|
96
|
+
progress_percent=100.0,
|
|
97
|
+
)
|
|
98
|
+
]
|
|
99
|
+
tracker.apply(later)
|
|
100
|
+
assert later[0].newly_spawned is True
|
|
70
101
|
|
|
71
102
|
|
|
72
103
|
def test_tracker_clears_newly_when_not_recent() -> None:
|
|
@@ -76,37 +107,105 @@ def test_tracker_clears_newly_when_not_recent() -> None:
|
|
|
76
107
|
assert marks[0].newly_spawned is False
|
|
77
108
|
|
|
78
109
|
|
|
79
|
-
def
|
|
110
|
+
def test_tracker_does_not_report_recent_before_100_percent() -> None:
|
|
80
111
|
tracker = SpawnReportTracker()
|
|
81
|
-
marks = [
|
|
112
|
+
marks = [
|
|
113
|
+
_mark(
|
|
114
|
+
recently=True,
|
|
115
|
+
phase=SpawnWindowPhase.OPEN,
|
|
116
|
+
progress_percent=99.0,
|
|
117
|
+
)
|
|
118
|
+
]
|
|
82
119
|
tracker.apply(marks)
|
|
83
|
-
assert marks[0].newly_opened is
|
|
84
|
-
assert marks[0].newly_spawned is
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
120
|
+
assert marks[0].newly_opened is False
|
|
121
|
+
assert marks[0].newly_spawned is False
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def test_tracker_does_not_report_open_window_before_100_percent() -> None:
|
|
125
|
+
tracker = SpawnReportTracker()
|
|
126
|
+
marks = [
|
|
127
|
+
_mark(
|
|
128
|
+
recently=False,
|
|
129
|
+
phase=SpawnWindowPhase.OPEN,
|
|
130
|
+
progress_percent=99.0,
|
|
131
|
+
)
|
|
132
|
+
]
|
|
133
|
+
tracker.apply(marks)
|
|
134
|
+
assert marks[0].newly_opened is False
|
|
135
|
+
assert marks[0].newly_spawned is False
|
|
94
136
|
|
|
95
137
|
|
|
138
|
+
def test_tracker_marks_first_100_percent_open_window_as_new() -> None:
|
|
139
|
+
tracker = SpawnReportTracker()
|
|
140
|
+
marks = [
|
|
141
|
+
_mark(
|
|
142
|
+
recently=False,
|
|
143
|
+
phase=SpawnWindowPhase.OPEN,
|
|
144
|
+
progress_percent=100.0,
|
|
145
|
+
)
|
|
146
|
+
]
|
|
147
|
+
tracker.apply(marks)
|
|
148
|
+
assert marks[0].newly_opened is True
|
|
149
|
+
assert marks[0].newly_spawned is True
|
|
150
|
+
|
|
151
|
+
packet = HuntCrawlPacket(
|
|
152
|
+
crawled_at=1.0,
|
|
153
|
+
next_fetch_at=2.0,
|
|
154
|
+
marks=marks,
|
|
155
|
+
query=HuntQueryFilter(),
|
|
156
|
+
)
|
|
157
|
+
assert packet.newly_opened_marks == marks
|
|
158
|
+
assert packet.newly_spawned_marks == marks
|
|
159
|
+
|
|
160
|
+
|
|
96
161
|
def test_tracker_skips_repeat_open_window_cycle() -> None:
|
|
97
162
|
tracker = SpawnReportTracker()
|
|
98
|
-
first = [
|
|
163
|
+
first = [
|
|
164
|
+
_mark(
|
|
165
|
+
recently=False,
|
|
166
|
+
phase=SpawnWindowPhase.OPEN,
|
|
167
|
+
progress_percent=100.0,
|
|
168
|
+
)
|
|
169
|
+
]
|
|
99
170
|
tracker.apply(first)
|
|
100
|
-
second = [
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
171
|
+
second = [
|
|
172
|
+
_mark(
|
|
173
|
+
recently=False,
|
|
174
|
+
phase=SpawnWindowPhase.OPEN,
|
|
175
|
+
progress_percent=100.0,
|
|
176
|
+
)
|
|
177
|
+
]
|
|
178
|
+
tracker.apply(second)
|
|
179
|
+
assert second[0].newly_opened is False
|
|
180
|
+
assert second[0].newly_spawned is False
|
|
181
|
+
|
|
182
|
+
|
|
106
183
|
def test_tracker_reports_spawn_after_open_window_cycle() -> None:
|
|
107
184
|
tracker = SpawnReportTracker()
|
|
108
|
-
tracker.apply(
|
|
109
|
-
|
|
185
|
+
tracker.apply(
|
|
186
|
+
[
|
|
187
|
+
_mark(
|
|
188
|
+
recently=False,
|
|
189
|
+
phase=SpawnWindowPhase.OPEN,
|
|
190
|
+
progress_percent=100.0,
|
|
191
|
+
)
|
|
192
|
+
]
|
|
193
|
+
)
|
|
194
|
+
later = [
|
|
195
|
+
_mark(
|
|
196
|
+
recently=True,
|
|
197
|
+
phase=SpawnWindowPhase.OPEN,
|
|
198
|
+
progress_percent=100.0,
|
|
199
|
+
)
|
|
200
|
+
]
|
|
110
201
|
tracker.apply(later)
|
|
111
202
|
assert later[0].newly_opened is False
|
|
112
203
|
assert later[0].newly_spawned is True
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_tracker_marks_capped_window_as_new() -> None:
|
|
207
|
+
tracker = SpawnReportTracker()
|
|
208
|
+
marks = [_mark(recently=False, phase=SpawnWindowPhase.CAPPED)]
|
|
209
|
+
tracker.apply(marks)
|
|
210
|
+
assert marks[0].newly_opened is True
|
|
211
|
+
assert marks[0].newly_spawned is True
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import struct
|
|
3
|
+
import zlib
|
|
4
|
+
|
|
5
|
+
from ff14_the_hunt.bear_tracker.resources import BearResources
|
|
6
|
+
from ff14_the_hunt.bear_tracker.timer_theme import build_timer_display
|
|
7
|
+
from ff14_the_hunt.models import (
|
|
8
|
+
HuntMarkRecord,
|
|
9
|
+
MapCoordinate,
|
|
10
|
+
SpawnWindowPhase,
|
|
11
|
+
TimerBarColor,
|
|
12
|
+
TimerKind,
|
|
13
|
+
)
|
|
14
|
+
from ff14_the_hunt.spawn_map.coordinates import with_pixel_coordinates
|
|
15
|
+
from ff14_the_hunt.spawn_map.attach import enrich_recent_spawn_details
|
|
16
|
+
from ff14_the_hunt.spawn_map.layout import layout_from_spawn_entry
|
|
17
|
+
from ff14_the_hunt.spawn_map.png_meta import read_png_dimensions
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _minimal_png(width: int, height: int) -> bytes:
|
|
21
|
+
signature = b"\x89PNG\r\n\x1a\n"
|
|
22
|
+
ihdr = struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0)
|
|
23
|
+
ihdr_chunk = (
|
|
24
|
+
b"IHDR"
|
|
25
|
+
+ ihdr
|
|
26
|
+
+ struct.pack(">I", zlib.crc32(b"IHDR" + ihdr) & 0xFFFFFFFF)
|
|
27
|
+
)
|
|
28
|
+
ihdr_len = struct.pack(">I", len(ihdr))
|
|
29
|
+
return signature + ihdr_len + ihdr_chunk
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_read_png_dimensions() -> None:
|
|
33
|
+
data = _minimal_png(2048, 1024)
|
|
34
|
+
assert read_png_dimensions(data) == (2048, 1024)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_layout_from_spawn_entry() -> None:
|
|
38
|
+
layout = layout_from_spawn_entry(
|
|
39
|
+
{
|
|
40
|
+
"Dimensions": [41, 41],
|
|
41
|
+
"DisplayPoints": 8,
|
|
42
|
+
"Version": 21,
|
|
43
|
+
},
|
|
44
|
+
)
|
|
45
|
+
assert layout is not None
|
|
46
|
+
assert layout.grid_scale == 41.0
|
|
47
|
+
assert layout.display_points == 8
|
|
48
|
+
assert layout.version == 21
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_with_pixel_coordinates() -> None:
|
|
52
|
+
points = [
|
|
53
|
+
MapCoordinate(point_key="SpawnPoint01", x=0.5, y=0.25),
|
|
54
|
+
]
|
|
55
|
+
enriched = with_pixel_coordinates(points, image_width=2000, image_height=1000)
|
|
56
|
+
assert enriched[0].pixel_x == 1000.0
|
|
57
|
+
assert enriched[0].pixel_y == 250.0
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_region_map_image_url_encodes_special_characters() -> None:
|
|
61
|
+
from ff14_the_hunt.spawn_map.region_fetch import region_map_image_url
|
|
62
|
+
|
|
63
|
+
url = region_map_image_url("https://tracker.beartoolkit.com", "Yak T'el")
|
|
64
|
+
assert (
|
|
65
|
+
url
|
|
66
|
+
== "https://tracker.beartoolkit.com/static/images/HuntRegions/Yak%20T%27el.png"
|
|
67
|
+
)
|
|
68
|
+
plain = region_map_image_url("https://tracker.beartoolkit.com", "Shaaloani")
|
|
69
|
+
assert plain == "https://tracker.beartoolkit.com/static/images/HuntRegions/Shaaloani.png"
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_build_region_map_image_round_trip() -> None:
|
|
73
|
+
from ff14_the_hunt.spawn_map.region_fetch import RegionMapFetcher
|
|
74
|
+
from ff14_the_hunt.spawn_map.region_image import build_region_map_image
|
|
75
|
+
|
|
76
|
+
png = _minimal_png(100, 80)
|
|
77
|
+
fetcher = RegionMapFetcher(site_root="https://tracker.beartoolkit.com")
|
|
78
|
+
fetcher._cache["Shaaloani"] = png
|
|
79
|
+
image = build_region_map_image(region="Shaaloani", fetcher=fetcher)
|
|
80
|
+
assert image is not None
|
|
81
|
+
assert image.width == 100
|
|
82
|
+
assert image.height == 80
|
|
83
|
+
assert base64.b64decode(image.data_base64) == png
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_open_window_before_100_percent_does_not_attach_spawn_details() -> None:
|
|
87
|
+
resources = _spawn_detail_resources()
|
|
88
|
+
mark = _open_window_mark(progress_percent=99.0)
|
|
89
|
+
enrich_recent_spawn_details(
|
|
90
|
+
[mark],
|
|
91
|
+
[_timer_row()],
|
|
92
|
+
resources=resources,
|
|
93
|
+
fetcher=None,
|
|
94
|
+
load_spawn_states=lambda _row, _resources: {},
|
|
95
|
+
include_region_map=False,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
assert mark.spawn_points == []
|
|
99
|
+
assert mark.spawn_map_layout is None
|
|
100
|
+
assert mark.region_map is None
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def test_recent_before_100_percent_does_not_attach_spawn_details() -> None:
|
|
104
|
+
resources = _spawn_detail_resources()
|
|
105
|
+
mark = _open_window_mark(progress_percent=99.0)
|
|
106
|
+
mark.recently_spawned = True
|
|
107
|
+
enrich_recent_spawn_details(
|
|
108
|
+
[mark],
|
|
109
|
+
[_timer_row()],
|
|
110
|
+
resources=resources,
|
|
111
|
+
fetcher=None,
|
|
112
|
+
load_spawn_states=lambda _row, _resources: {},
|
|
113
|
+
include_region_map=False,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
assert mark.spawn_points == []
|
|
117
|
+
assert mark.spawn_map_layout is None
|
|
118
|
+
assert mark.region_map is None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_100_percent_open_window_attaches_spawn_details_and_region_map() -> None:
|
|
122
|
+
from ff14_the_hunt.spawn_map.region_fetch import RegionMapFetcher
|
|
123
|
+
|
|
124
|
+
resources = _spawn_detail_resources()
|
|
125
|
+
mark = _open_window_mark(progress_percent=100.0)
|
|
126
|
+
fetcher = RegionMapFetcher(site_root="https://tracker.beartoolkit.com")
|
|
127
|
+
fetcher._cache["Living Memory"] = _minimal_png(100, 80)
|
|
128
|
+
enrich_recent_spawn_details(
|
|
129
|
+
[mark],
|
|
130
|
+
[_timer_row()],
|
|
131
|
+
resources=resources,
|
|
132
|
+
fetcher=fetcher,
|
|
133
|
+
load_spawn_states=lambda _row, _resources: {
|
|
134
|
+
"SpawnPoint01": {"State": False, "Verified": True},
|
|
135
|
+
},
|
|
136
|
+
include_region_map=True,
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
assert [point.point_key for point in mark.spawn_points] == ["SpawnPoint02"]
|
|
140
|
+
assert mark.spawn_points[0].pixel_x is not None
|
|
141
|
+
assert mark.spawn_points[0].pixel_y is not None
|
|
142
|
+
assert mark.region_map is not None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def _spawn_detail_resources() -> BearResources:
|
|
146
|
+
return BearResources(
|
|
147
|
+
{
|
|
148
|
+
"DatabaseHunt": {
|
|
149
|
+
"The Forecaster": {
|
|
150
|
+
"Patch": "DT",
|
|
151
|
+
"Region": "Living Memory",
|
|
152
|
+
"Rank": 3,
|
|
153
|
+
"RespawnTimer": [46.0, 2.0],
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
"SpawnPoint": {
|
|
157
|
+
"The Forecaster": {
|
|
158
|
+
"Dimensions": [41, 41],
|
|
159
|
+
"DisplayPoints": 2,
|
|
160
|
+
"SpawnPoint01": [1, 1],
|
|
161
|
+
"SpawnPoint02": [21, 21],
|
|
162
|
+
},
|
|
163
|
+
},
|
|
164
|
+
"DataCenters": {},
|
|
165
|
+
},
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _timer_row() -> dict:
|
|
170
|
+
return {
|
|
171
|
+
"huntKey": "The Forecaster",
|
|
172
|
+
"huntName": "The Forecaster",
|
|
173
|
+
"worldName": "Test",
|
|
174
|
+
"lastDeathTime": 1000.0,
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _open_window_mark(*, progress_percent: float) -> HuntMarkRecord:
|
|
179
|
+
return HuntMarkRecord(
|
|
180
|
+
hunt_key="The Forecaster",
|
|
181
|
+
hunt_name="The Forecaster",
|
|
182
|
+
world_name="Test",
|
|
183
|
+
region="Living Memory",
|
|
184
|
+
patch="DT",
|
|
185
|
+
last_death_time=1000.0,
|
|
186
|
+
trigger_timer=build_timer_display(
|
|
187
|
+
kind=TimerKind.TRIGGER,
|
|
188
|
+
phase=SpawnWindowPhase.OPEN,
|
|
189
|
+
bar_color=TimerBarColor.SUCCESS,
|
|
190
|
+
counts_up=True,
|
|
191
|
+
progress_percent=progress_percent,
|
|
192
|
+
summary="test",
|
|
193
|
+
),
|
|
194
|
+
)
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import base64
|
|
2
|
-
import struct
|
|
3
|
-
import zlib
|
|
4
|
-
|
|
5
|
-
from ff14_the_hunt.models import MapCoordinate
|
|
6
|
-
from ff14_the_hunt.spawn_map.coordinates import with_pixel_coordinates
|
|
7
|
-
from ff14_the_hunt.spawn_map.layout import layout_from_spawn_entry
|
|
8
|
-
from ff14_the_hunt.spawn_map.png_meta import read_png_dimensions
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def _minimal_png(width: int, height: int) -> bytes:
|
|
12
|
-
signature = b"\x89PNG\r\n\x1a\n"
|
|
13
|
-
ihdr = struct.pack(">IIBBBBB", width, height, 8, 2, 0, 0, 0)
|
|
14
|
-
ihdr_chunk = (
|
|
15
|
-
b"IHDR"
|
|
16
|
-
+ ihdr
|
|
17
|
-
+ struct.pack(">I", zlib.crc32(b"IHDR" + ihdr) & 0xFFFFFFFF)
|
|
18
|
-
)
|
|
19
|
-
ihdr_len = struct.pack(">I", len(ihdr))
|
|
20
|
-
return signature + ihdr_len + ihdr_chunk
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def test_read_png_dimensions() -> None:
|
|
24
|
-
data = _minimal_png(2048, 1024)
|
|
25
|
-
assert read_png_dimensions(data) == (2048, 1024)
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def test_layout_from_spawn_entry() -> None:
|
|
29
|
-
layout = layout_from_spawn_entry(
|
|
30
|
-
{
|
|
31
|
-
"Dimensions": [41, 41],
|
|
32
|
-
"DisplayPoints": 8,
|
|
33
|
-
"Version": 21,
|
|
34
|
-
},
|
|
35
|
-
)
|
|
36
|
-
assert layout is not None
|
|
37
|
-
assert layout.grid_scale == 41.0
|
|
38
|
-
assert layout.display_points == 8
|
|
39
|
-
assert layout.version == 21
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def test_with_pixel_coordinates() -> None:
|
|
43
|
-
points = [
|
|
44
|
-
MapCoordinate(point_key="SpawnPoint01", x=0.5, y=0.25),
|
|
45
|
-
]
|
|
46
|
-
enriched = with_pixel_coordinates(points, image_width=2000, image_height=1000)
|
|
47
|
-
assert enriched[0].pixel_x == 1000.0
|
|
48
|
-
assert enriched[0].pixel_y == 250.0
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
def test_region_map_image_url_encodes_special_characters() -> None:
|
|
52
|
-
from ff14_the_hunt.spawn_map.region_fetch import region_map_image_url
|
|
53
|
-
|
|
54
|
-
url = region_map_image_url("https://tracker.beartoolkit.com", "Yak T'el")
|
|
55
|
-
assert (
|
|
56
|
-
url
|
|
57
|
-
== "https://tracker.beartoolkit.com/static/images/HuntRegions/Yak%20T%27el.png"
|
|
58
|
-
)
|
|
59
|
-
plain = region_map_image_url("https://tracker.beartoolkit.com", "Shaaloani")
|
|
60
|
-
assert plain == "https://tracker.beartoolkit.com/static/images/HuntRegions/Shaaloani.png"
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def test_build_region_map_image_round_trip() -> None:
|
|
64
|
-
from ff14_the_hunt.spawn_map.region_fetch import RegionMapFetcher
|
|
65
|
-
from ff14_the_hunt.spawn_map.region_image import build_region_map_image
|
|
66
|
-
|
|
67
|
-
png = _minimal_png(100, 80)
|
|
68
|
-
fetcher = RegionMapFetcher(site_root="https://tracker.beartoolkit.com")
|
|
69
|
-
fetcher._cache["Shaaloani"] = png
|
|
70
|
-
image = build_region_map_image(region="Shaaloani", fetcher=fetcher)
|
|
71
|
-
assert image is not None
|
|
72
|
-
assert image.width == 100
|
|
73
|
-
assert image.height == 80
|
|
74
|
-
assert base64.b64decode(image.data_base64) == png
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/example/__main__.py
RENAMED
|
File without changes
|
{python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/example/spawn_map_io.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/tests/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/tests/test_fate_timer.py
RENAMED
|
File without changes
|
|
File without changes
|
{python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/tests/test_locale_cn.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{python_library_ff14_the_hunt-0.0.10 → python_library_ff14_the_hunt-0.0.11}/tests/test_poll_sleep.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|