1use encoding_rs::SHIFT_JIS;
16use regex::Regex;
17use std::sync::OnceLock;
18
19use crate::proxy::{send_follow_redirects, url_encode, BoxError, ProxyMode};
20
21pub type TileId = u32;
28
29const SERVER_IMAGE_BASE: &str = "http://www7019ug.sakura.ne.jp/CHaserOnline003/img/";
30
31pub fn tile_image_url(tile: TileId) -> String {
33 format!("{}{:03}.gif", SERVER_IMAGE_BASE, tile)
34}
35
36#[derive(Debug, Clone)]
38pub struct PlayerInfo {
39 pub username: String,
40 pub attr_a: i32,
41 pub attr_i: i32,
42 pub attr_p: i32,
43 pub attr_pd: i32,
44 pub attr_t: i32,
45 pub commands: Vec<String>,
47}
48
49#[derive(Debug, Clone)]
51pub struct MapViewResult {
52 pub room_name: String,
53 pub turn: u32,
54 pub next_player: String,
55 pub map: Vec<Vec<TileId>>,
57 pub players: Vec<PlayerInfo>,
58}
59
60#[derive(Debug, Clone, Default)]
62pub struct MapViewOptions {
63 pub proxy_uri: Option<String>,
65}
66
67fn re_img_src() -> &'static Regex {
72 static RE: OnceLock<Regex> = OnceLock::new();
73 RE.get_or_init(|| Regex::new(r"/img/(\d{1,3})\.gif").expect("re_img_src"))
74}
75
76fn re_turn() -> &'static Regex {
77 static RE: OnceLock<Regex> = OnceLock::new();
78 RE.get_or_init(|| Regex::new(r"turn=(\d+)\s+Next=([^<\s]+)").expect("re_turn"))
79}
80
81fn re_room_name() -> &'static Regex {
82 static RE: OnceLock<Regex> = OnceLock::new();
83 RE.get_or_init(|| Regex::new(r"(?i)<h1[^>]*>[^\[]*\[([^\]]+)\]").expect("re_room_name"))
84}
85
86fn inner_text<'a>(node: &tl::Node<'a>, parser: &'a tl::Parser<'a>) -> String {
91 match node {
92 tl::Node::Raw(b) => b.as_utf8_str().into_owned(),
93 tl::Node::Tag(tag) => tag
94 .children().top().iter()
95 .filter_map(|h| h.get(parser))
96 .map(|n| inner_text(n, parser))
97 .collect(),
98 _ => String::new(),
99 }
100}
101
102pub(crate) fn parse_room_name(html: &str) -> String {
103 re_room_name()
104 .captures(html)
105 .and_then(|c| c.get(1))
106 .map(|m| m.as_str().trim().to_string())
107 .unwrap_or_default()
108}
109
110pub(crate) fn parse_map(html: &str) -> (Vec<Vec<TileId>>, u32, String) {
111 let (turn, next_player) = re_turn()
112 .captures(html)
113 .map(|c| (
114 c.get(1).and_then(|m| m.as_str().parse().ok()).unwrap_or(0),
115 c.get(2).map(|m| m.as_str().to_string()).unwrap_or_default(),
116 ))
117 .unwrap_or((0, String::new()));
118
119 let lower = html.to_ascii_lowercase();
120 let table_start = match lower.find(r#"cellpadding="0""#)
121 .and_then(|i| html[..i].rfind('<'))
122 {
123 Some(i) => i,
124 None => return (vec![], turn, next_player),
125 };
126 let table_end = match lower[table_start..].find("</table") {
127 Some(i) => table_start + i,
128 None => return (vec![], turn, next_player),
129 };
130 let table_html = &html[table_start..table_end];
131 let table_lower = &lower[table_start..table_end];
132
133 let tr_positions: Vec<usize> = table_lower
134 .match_indices("<tr")
135 .map(|(i, _)| i)
136 .collect();
137
138 let mut map: Vec<Vec<TileId>> = Vec::new();
139 for (idx, &tr_start) in tr_positions.iter().enumerate() {
140 let tr_end = tr_positions.get(idx + 1).copied().unwrap_or(table_html.len());
141 let tr_slice = &table_html[tr_start..tr_end];
142
143 let row: Vec<TileId> = re_img_src()
144 .captures_iter(tr_slice)
145 .filter_map(|c| c.get(1).and_then(|m| {
146 let id: u32 = m.as_str().parse().ok()?;
147 if id >= 1000 { return None; }
148 Some(id)
149 }))
150 .collect();
151
152 if !row.is_empty() {
153 map.push(row);
154 }
155 }
156
157 (map, turn, next_player)
158}
159
160pub(crate) fn parse_players(dom: &tl::VDom) -> Vec<PlayerInfo> {
161 let parser = dom.parser();
162
163 let player_table = match dom
164 .query_selector(r#"table[border="1"]"#)
165 .and_then(|mut q| q.next())
166 .and_then(|h| h.get(parser))
167 {
168 Some(tl::Node::Tag(t)) => t,
169 _ => return vec![],
170 };
171
172 let table_html: String = player_table
173 .children().top().iter()
174 .filter_map(|h| h.get(parser))
175 .map(|n| n.outer_html(parser).to_string())
176 .collect();
177
178 let Ok(dom2) = tl::parse(&table_html, tl::ParserOptions::default()) else {
179 return vec![];
180 };
181 let p2 = dom2.parser();
182
183 let tr_htmls: Vec<String> = dom2
184 .query_selector("tr").into_iter().flatten()
185 .filter_map(|h| h.get(p2))
186 .filter_map(|n| match n {
187 tl::Node::Tag(t) => Some(
188 t.children().top().iter()
189 .filter_map(|h| h.get(p2))
190 .map(|n| n.outer_html(p2).to_string())
191 .collect::<String>()
192 ),
193 _ => None,
194 })
195 .collect();
196
197 let mut player_data: Vec<(String, i32, i32, i32, i32, i32)> = Vec::new();
198 if let Some(tr0) = tr_htmls.first() {
199 if let Ok(d) = tl::parse(tr0, tl::ParserOptions::default()) {
200 let p = d.parser();
201 let td_htmls: Vec<String> = d.query_selector(r#"td[valign="top"]"#)
202 .into_iter().flatten()
203 .filter_map(|h| h.get(p))
204 .filter_map(|n| match n {
205 tl::Node::Tag(t) => Some(
206 t.children().top().iter()
207 .filter_map(|h| h.get(p))
208 .map(|n| n.outer_html(p).to_string())
209 .collect::<String>()
210 ),
211 _ => None,
212 })
213 .collect();
214
215 for td_html in &td_htmls {
216 if let Ok(td_dom) = tl::parse(td_html, tl::ParserOptions::default()) {
217 let tp = td_dom.parser();
218 let text: String = td_dom.nodes().iter()
219 .map(|n| inner_text(n, tp))
220 .collect::<Vec<_>>()
221 .join(" ");
222
223 let mut username = String::new();
224 let mut attr_a = 0i32;
225 let mut attr_i = 0i32;
226 let mut attr_p = 0i32;
227 let mut attr_pd = 0i32;
228 let mut attr_t = 0i32;
229 let mut found_any = false;
230
231 for token in text.split_whitespace() {
232 if let Some(v) = token.strip_prefix("A:") {
233 attr_a = v.parse().unwrap_or(0); found_any = true;
234 } else if let Some(v) = token.strip_prefix("I:") {
235 attr_i = v.parse().unwrap_or(0);
236 } else if let Some(v) = token.strip_prefix("PD:") {
237 attr_pd = v.parse().unwrap_or(0);
238 } else if let Some(v) = token.strip_prefix("P:") {
239 attr_p = v.trim_end_matches(']').parse().unwrap_or(0);
240 } else if let Some(v) = token.strip_prefix("T:") {
241 attr_t = v.trim_end_matches(']').parse().unwrap_or(0);
242 } else if username.is_empty() && !token.starts_with('[') && !token.is_empty() {
243 username = token.to_string();
244 }
245 }
246 if found_any {
247 player_data.push((username, attr_a, attr_i, attr_p, attr_pd, attr_t));
248 }
249 }
250 }
251 }
252 }
253
254 let mut all_commands: Vec<Vec<String>> = Vec::new();
255 if let Some(tr1) = tr_htmls.get(1) {
256 if let Ok(d) = tl::parse(tr1, tl::ParserOptions::default()) {
257 let p = d.parser();
258 let font_htmls: Vec<String> = d.query_selector("font")
259 .into_iter().flatten()
260 .filter_map(|h| h.get(p))
261 .filter_map(|n| match n {
262 tl::Node::Tag(t) => Some(
263 t.children().top().iter()
264 .filter_map(|h| h.get(p))
265 .map(|n| n.outer_html(p).to_string())
266 .collect::<String>()
267 ),
268 _ => None,
269 })
270 .collect();
271
272 for font_html in &font_htmls {
273 if let Ok(fd) = tl::parse(font_html, tl::ParserOptions::default()) {
274 let fp = fd.parser();
275 let text = fd.nodes().iter()
276 .map(|n| inner_text(n, fp))
277 .collect::<String>();
278 let cmds: Vec<String> = text.lines()
279 .map(|l| l.trim().to_string())
280 .filter(|l| !l.is_empty())
281 .collect();
282 all_commands.push(cmds);
283 }
284 }
285 }
286 }
287
288 let mut players = Vec::new();
289 for (i, (username, attr_a, attr_i, attr_p, attr_pd, attr_t)) in player_data.into_iter().enumerate() {
290 let commands = all_commands.get(i).cloned().unwrap_or_default();
291 players.push(PlayerInfo { username, attr_a, attr_i, attr_p, attr_pd, attr_t, commands });
292 }
293 players
294}
295
296const SERVER_CHECK_URL: &str =
301 "http://www7019ug.sakura.ne.jp/CHaserOnline003/Server/UserCheck";
302const MAP_VIEW_URL: &str =
303 "http://www7019ug.sakura.ne.jp/CHaserOnline003/Server/MapView.jsp";
304
305async fn fetch_inner(
310 user: &str,
311 pass: &str,
312 proxy_mode: ProxyMode,
313) -> Result<MapViewResult, BoxError> {
314 let check_url = format!(
316 "{}?user={}&pass={}&select=mapview",
317 SERVER_CHECK_URL,
318 url_encode(user),
319 url_encode(pass),
320 );
321 let (_, jsession) = send_follow_redirects(&check_url, &[], &proxy_mode).await?;
322 let jsessionid = jsession.ok_or("Server JSESSIONID not found")?;
323 let cookie = format!("JSESSIONID={}", jsessionid);
324
325 let (body, _) =
326 send_follow_redirects(MAP_VIEW_URL, &[("Cookie", cookie)], &proxy_mode).await?;
327 let (html, _, _) = SHIFT_JIS.decode(&body);
328
329 let room_name = parse_room_name(&html);
330 let (map, turn, next_player) = parse_map(&html);
331 let dom = tl::parse(&html, tl::ParserOptions::default())?;
332 let players = parse_players(&dom);
333
334 Ok(MapViewResult { room_name, turn, next_player, map, players })
335}
336
337pub async fn fetch_map_view(
343 user: &str,
344 pass: &str,
345 opts: MapViewOptions,
346) -> Result<MapViewResult, BoxError> {
347 let proxy_mode = ProxyMode::from_option(opts.proxy_uri.as_deref());
348 fetch_inner(user, pass, proxy_mode).await
349}