|
129 | 129 | const isIpv6HostText=(h)=>{const s=String(h||"").trim();if(!s)return false;if(s.startsWith("[")&&s.endsWith("]"))return true;return s.includes(":")&&!/^[^:]+:\d+$/.test(s)}; |
130 | 130 | const isLocalHost=(h)=>{const s=String(h||"").trim().toLowerCase();return s==="localhost"||s==="127.0.0.1"||s==="::1"||s==="[::1]"||s==="0:0:0:0:0:0:0:1"}; |
131 | 131 | const pageIsHttps=(typeof location!=="undefined"&&location.protocol==="https:"); |
132 | | - const defaultWsProtoForHost=(h)=>{const hs=String(h||"").trim().toLowerCase();if(pageIsHttps&&!isLocalHost(hs))return"wss";return"ws"}; |
133 | | - const resolveConnectProto=(host,proto)=>{let p=(String(proto||"").toLowerCase()==="wss")?"wss":"ws";if(p==="ws"&&pageIsHttps&&!isLocalHost(host))p="wss";return p}; |
| 132 | + const defaultWsProtoForHost=(_h)=>"ws"; |
| 133 | + const resolveConnectProto=(_host,proto)=>((String(proto||"").toLowerCase()==="wss")?"wss":"ws"); |
134 | 134 | const normWsPath=(raw)=>{let p=String(raw??"").trim();if(!p||p==="/")return"";if(p.startsWith("?"))p=`/${p}`;if(!p.startsWith("/"))p=`/${p}`;p=p.replace(/\/{2,}/g,"/");return p}; |
135 | 135 | const hostSpec=(raw,fallbackProto="auto")=>{ |
136 | 136 | let s=String(raw??"").trim(),proto="",port=null,path="",protoExplicit=false; |
|
144 | 144 | protoExplicit=true; |
145 | 145 | const sch=String(m[1]||"").toLowerCase(); |
146 | 146 | s=String(m[2]||""); |
147 | | - if(sch==="ws"||sch==="wss")proto=sch;else if(sch==="http")proto="ws";else if(sch==="https")proto="wss"; |
| 147 | + if(sch==="ws"||sch==="wss")proto=sch;else if(sch==="http")proto="ws";else if(sch==="https")proto="ws"; |
148 | 148 | } |
149 | 149 | const slash=s.indexOf("/"); |
150 | 150 | if(slash>=0){path=s.slice(slash);s=s.slice(0,slash)} |
|
162 | 162 | } |
163 | 163 | s=s.trim().toLowerCase(); |
164 | 164 | if(!s)s="127.0.0.1"; |
165 | | - if(!proto&&port===443)proto="wss"; |
166 | 165 | if(!proto){ |
167 | 166 | const fb=(fallbackProto==="auto")?defaultWsProtoForHost(s):fallbackProto; |
168 | 167 | proto=(String(fb).toLowerCase()==="wss")?"wss":"ws"; |
169 | 168 | } |
170 | | - proto=resolveConnectProto(s,proto); |
171 | 169 | if(!Number.isFinite(port)||port<=0)port=null; |
172 | 170 | return{host:s,proto,port,path:normWsPath(path)}; |
173 | 171 | }; |
|
316 | 314 | } |
317 | 315 | return dedupeBy(plans,(x)=>`${x.proto}|${nh(x.host)}|${i32(x.port,0)}|${normWsPath(x.path)}|${i32(x.roomPortHint,0)}`); |
318 | 316 | }; |
319 | | - const FIREWALL_HINT="默认优先 443/WSS(更不易被防火墙拦截);局域网或未配置 TLS 时可改用 WS/5000。若网页通过 HTTPS 打开,浏览器会阻止 ws:// 非本机地址。"; |
| 317 | + const FIREWALL_HINT="默认建议 WS/5000。只有在服务器已配置 TLS 证书时才使用 WSS(常见为 443)。若网页通过 HTTPS 打开,浏览器可能阻止 ws:// 非本机地址。"; |
320 | 318 | class BrowserClientConnection{ |
321 | 319 | constructor({host,port,name,resumeIndex=null,resumeKey="",wsProto="ws",wsPath="",roomPortHint=0}){this.host=nh(host);this.port=i32(port,0);this.name=String(name||"玩家");this.ws_proto=(String(wsProto||"").toLowerCase()==="wss")?"wss":"ws";this.ws_path=normWsPath(wsPath);this.room_port_hint=i32(roomPortHint,0);this.session_port=this.room_port_hint>0?this.room_port_hint:this.port;this.ws=null;this.state=null;this.error=null;this.my_index=null;this.room_size=null;this.running=false;this.resume_index=resumeIndex;this.resume_key=String(resumeKey||"").trim();this._hbTimer=null} |
322 | 320 | _send_packet(p){if(!this.ws||this.ws.readyState!==WebSocket.OPEN)throw new Error("尚未连接到房间。");this.ws.send(JSON.stringify(p))} |
|
372 | 370 | async _show_access(host,port){const d=mkDialog("房间信息"),body=d.querySelector(".mbody"),btns=d.querySelector(".mbtns");const l1=document.createElement("div");l1.className="mline";l1.innerHTML='<label>IP地址</label><textarea readonly></textarea>';l1.querySelector("textarea").value=String(host);body.appendChild(l1);const l2=document.createElement("div");l2.className="mline";l2.innerHTML='<label>端口</label><textarea readonly></textarea>';l2.querySelector("textarea").value=String(port);body.appendChild(l2); |
373 | 371 | const row=document.createElement("div");row.className="mbtns";row.style.padding="0";const b1=document.createElement("button");b1.textContent="复制IP";b1.onclick=async()=>{try{await navigator.clipboard.writeText(String(host));this._status(`已复制IP:${host}`)}catch(_){this._status("IP复制失败。")}};const b2=document.createElement("button");b2.textContent="复制端口";b2.onclick=async()=>{try{await navigator.clipboard.writeText(String(port));this._status(`已复制端口:${port}`)}catch(_){this._status("端口复制失败。")}};row.appendChild(b1);row.appendChild(b2);body.appendChild(row);const note=document.createElement("div");note.className="mnote";note.textContent="房间信息仅在此处展示,请及时复制。";body.appendChild(note); |
374 | 372 | btns.classList.add("single");const c=document.createElement("button");c.textContent="关闭";c.onclick=()=>closeDialog(d);btns.appendChild(c);d.addEventListener("cancel",e=>{e.preventDefault();closeDialog(d)});d.showModal()} |
375 | | - async _open_remote(){const d=mkDialog("向服务器申请联机"),body=d.querySelector(".mbody"),btns=d.querySelector(".mbtns"),spinFrames=["◐","◓","◑","◒"];const addLine=(label,type,value,min,max,step)=>{const line=document.createElement("div");line.className="mline";const lab=document.createElement("label");lab.textContent=label;const ip=document.createElement("input");ip.type=type;ip.value=String(value??"");if(type==="number"){if(min!==undefined)ip.min=String(min);if(max!==undefined)ip.max=String(max);ip.step=String(step??1)}line.appendChild(lab);line.appendChild(ip);body.appendChild(line);return ip};const ipHost=addLine("服务器IP","text",this.remoteServerHost),ipName=addLine("你的昵称","text",this.remoteName),ipRoom=addLine("房间人数","number",this.remoteRoomSize,2,8,1),ipHand=addLine("初始手牌数","number",this.remoteHandSize,2,10,1);const note=document.createElement("div");note.className="mnote";note.textContent=`所有请求都由当前设备浏览器直接发起,不经过网页部署服务器。默认会自动择优:HTTP 页面优先 ws://,HTTPS 页面(非本机)会自动改用 wss://;你也可以手动输入 ws:// 或 wss:// 覆盖。\n${FIREWALL_HINT}`;body.appendChild(note);const inputs=[ipHost,ipName,ipRoom,ipHand];const btnCancel=document.createElement("button");btnCancel.textContent="取消";const btnOk=document.createElement("button");btnOk.textContent="申请并加入对局";btns.appendChild(btnCancel);btns.appendChild(btnOk);let pending=false,spinTimer=null,spinIdx=0;const setPending=(on)=>{pending=!!on;for(const ip of inputs)ip.disabled=pending;btnCancel.disabled=pending;btnOk.disabled=pending;if(spinTimer!==null){clearInterval(spinTimer);spinTimer=null}if(pending){btnOk.textContent=`${spinFrames[0]} 申请并加入对局`;spinIdx=1;spinTimer=setInterval(()=>{if(!pending)return;btnOk.textContent=`${spinFrames[spinIdx%spinFrames.length]} 申请并加入对局`;spinIdx++},120)}else btnOk.textContent="申请并加入对局"};const closeIfIdle=()=>{if(pending)return;closeDialog(d)};btnCancel.onclick=()=>closeIfIdle();d.addEventListener("cancel",e=>{e.preventDefault();closeIfIdle()});btnOk.onclick=async()=>{if(pending)return;const hs=hostSpec(ipHost.value),host=hs.host,proto=hs.proto,path=hs.path,serverPort=i32(hs.port,0),name=String(ipName.value||"玩家").trim()||"玩家",rs=i32(ipRoom.value,3),hcnt=i32(ipHand.value,10);if(serverPort>0&&serverPort!==5000&&serverPort!==443){await alertDialog("错误","服务器主端口建议使用 5000 或 443。请仅填写 IP,或填写 IP:5000 / IP:443。");return}if(rs<2||rs>8){await alertDialog("错误","远程房间人数必须在 2~8 之间。");return}if(hcnt<2||hcnt>10){await alertDialog("错误","远程初始手牌数必须在 2~10 之间。");return}this.remoteServerHost=host;this.remoteWsProto=proto;this.remotePath=path;this.remoteName=name;this.remoteRoomSize=rs;this.remoteHandSize=hcnt;note.textContent="正在向服务器申请房间,请稍候...";setPending(true);let ok=false;try{ok=await this._start_remote()}finally{setPending(false)}if(ok){note.textContent="申请成功,可继续在本窗口修改参数后再次申请。";closeDialog(d)}else note.textContent="申请失败,请修改参数后重试。"};d.showModal();ipHost.focus();ipHost.select()} |
| 373 | + async _open_remote(){const d=mkDialog("向服务器申请联机"),body=d.querySelector(".mbody"),btns=d.querySelector(".mbtns"),spinFrames=["◐","◓","◑","◒"];const addLine=(label,type,value,min,max,step)=>{const line=document.createElement("div");line.className="mline";const lab=document.createElement("label");lab.textContent=label;const ip=document.createElement("input");ip.type=type;ip.value=String(value??"");if(type==="number"){if(min!==undefined)ip.min=String(min);if(max!==undefined)ip.max=String(max);ip.step=String(step??1)}line.appendChild(lab);line.appendChild(ip);body.appendChild(line);return ip};const ipHost=addLine("服务器IP","text",this.remoteServerHost),ipName=addLine("你的昵称","text",this.remoteName),ipRoom=addLine("房间人数","number",this.remoteRoomSize,2,8,1),ipHand=addLine("初始手牌数","number",this.remoteHandSize,2,10,1);const note=document.createElement("div");note.className="mnote";note.textContent=`所有请求都由当前设备浏览器直接发起,不经过网页部署服务器。默认使用 ws://;仅在你显式输入 wss:// 时才使用加密连接。\n${FIREWALL_HINT}`;body.appendChild(note);const inputs=[ipHost,ipName,ipRoom,ipHand];const btnCancel=document.createElement("button");btnCancel.textContent="取消";const btnOk=document.createElement("button");btnOk.textContent="申请并加入对局";btns.appendChild(btnCancel);btns.appendChild(btnOk);let pending=false,spinTimer=null,spinIdx=0;const setPending=(on)=>{pending=!!on;for(const ip of inputs)ip.disabled=pending;btnCancel.disabled=pending;btnOk.disabled=pending;if(spinTimer!==null){clearInterval(spinTimer);spinTimer=null}if(pending){btnOk.textContent=`${spinFrames[0]} 申请并加入对局`;spinIdx=1;spinTimer=setInterval(()=>{if(!pending)return;btnOk.textContent=`${spinFrames[spinIdx%spinFrames.length]} 申请并加入对局`;spinIdx++},120)}else btnOk.textContent="申请并加入对局"};const closeIfIdle=()=>{if(pending)return;closeDialog(d)};btnCancel.onclick=()=>closeIfIdle();d.addEventListener("cancel",e=>{e.preventDefault();closeIfIdle()});btnOk.onclick=async()=>{if(pending)return;const hs=hostSpec(ipHost.value),host=hs.host,proto=hs.proto,path=hs.path,serverPort=i32(hs.port,0),name=String(ipName.value||"玩家").trim()||"玩家",rs=i32(ipRoom.value,3),hcnt=i32(ipHand.value,10);if(serverPort>0&&serverPort!==5000&&serverPort!==443){await alertDialog("错误","服务器主端口建议使用 5000 或 443。请仅填写 IP,或填写 IP:5000 / IP:443。");return}if(rs<2||rs>8){await alertDialog("错误","远程房间人数必须在 2~8 之间。");return}if(hcnt<2||hcnt>10){await alertDialog("错误","远程初始手牌数必须在 2~10 之间。");return}this.remoteServerHost=host;this.remoteWsProto=proto;this.remotePath=path;this.remoteName=name;this.remoteRoomSize=rs;this.remoteHandSize=hcnt;note.textContent="正在向服务器申请房间,请稍候...";setPending(true);let ok=false;try{ok=await this._start_remote()}finally{setPending(false)}if(ok){note.textContent="申请成功,可继续在本窗口修改参数后再次申请。";closeDialog(d)}else note.textContent="申请失败,请修改参数后重试。"};d.showModal();ipHost.focus();ipHost.select()} |
376 | 374 | async _open_join(){const d=mkDialog("加入服务器房间"),body=d.querySelector(".mbody"),btns=d.querySelector(".mbtns"),spinFrames=["◐","◓","◑","◒"];const addLine=(label,type,value,min,max,step)=>{const line=document.createElement("div");line.className="mline";const lab=document.createElement("label");lab.textContent=label;const ip=document.createElement("input");ip.type=type;ip.value=String(value??"");if(type==="number"){if(min!==undefined)ip.min=String(min);if(max!==undefined)ip.max=String(max);ip.step=String(step??1)}line.appendChild(lab);line.appendChild(ip);body.appendChild(line);return ip};const ipName=addLine("你的昵称","text",this.joinName),ipHost=addLine("服务器地址","text",this.joinHost),ipPort=addLine("对局端口","number",this.joinPort,1,65535,1);const note=document.createElement("div");note.className="mnote";note.textContent=`连接请求由当前设备浏览器直接发起。注意:5000 是主端口,不是对局端口。\n${FIREWALL_HINT}`;body.appendChild(note);const inputs=[ipName,ipHost,ipPort];const btnCancel=document.createElement("button");btnCancel.textContent="取消";const btnOk=document.createElement("button");btnOk.textContent="加入对局";btns.appendChild(btnCancel);btns.appendChild(btnOk);let pending=false,spinTimer=null,spinIdx=0;const setPending=(on)=>{pending=!!on;for(const ip of inputs)ip.disabled=pending;btnCancel.disabled=pending;btnOk.disabled=pending;if(spinTimer!==null){clearInterval(spinTimer);spinTimer=null}if(pending){btnOk.textContent=`${spinFrames[0]} 加入对局`;spinIdx=1;spinTimer=setInterval(()=>{if(!pending)return;btnOk.textContent=`${spinFrames[spinIdx%spinFrames.length]} 加入对局`;spinIdx++},120)}else btnOk.textContent="加入对局"};const closeIfIdle=()=>{if(pending)return;closeDialog(d)};btnCancel.onclick=()=>closeIfIdle();d.addEventListener("cancel",e=>{e.preventDefault();closeIfIdle()});btnOk.onclick=async()=>{if(pending)return;const name=String(ipName.value||"玩家").trim()||"玩家",hs=hostSpec(ipHost.value),host=hs.host,proto=hs.proto,path=hs.path,embeddedPort=i32(hs.port,0),rawPort=String(ipPort.value??"").trim();let port=0;if(rawPort){port=i32(rawPort,0)}else if(embeddedPort>0){port=embeddedPort}if(embeddedPort>0&&rawPort&&port!==embeddedPort){await alertDialog("错误","地址中的端口与“对局端口”输入不一致,请统一后重试。");return}if(port<=0){await alertDialog("错误","端口输入无效。");return}if(port===5000){await alertDialog("错误","5000 是主端口,不能直接加入对局。请先“向服务器申请联机”或输入房主给出的对局端口。");return}this.joinName=name;this.joinHost=host;this.joinWsProto=proto;this.joinPath=path;this.joinPort=port;note.textContent="正在连接房间并等待服务器确认,请稍候...";setPending(true);let ok=false;try{ok=await this._join()}finally{setPending(false)}if(ok){note.textContent="连接成功,可继续在本窗口修改参数后再次加入。";closeDialog(d)}else note.textContent="连接失败,请检查服务器地址和端口后重试。"};d.showModal();ipName.focus();ipName.select()} |
377 | 375 |
|
378 | 376 | _cur_hand(){if(!this.lastState)return{};const you=i32(this.lastState.you_index,-1),ps=Array.isArray(this.lastState.players)?this.lastState.players:[];if(you<0||you>=ps.length)return{};const h=ps[you]?.hand;return(h&&typeof h==="object")?h:{}} |
|
0 commit comments