mirror of
https://github.com/WiiLink24/wfc-server.git
synced 2026-03-21 17:44:58 -05:00
Go-ify and improve some code
This commit is contained in:
parent
825b67405b
commit
08f85da5f4
|
|
@ -84,7 +84,9 @@ func MarshalNASAuthToken(gamecd string, userid uint64, gsbrcd string, cfc uint64
|
|||
return "NDS" + Base64DwcEncoding.EncodeToString(blob), challenge
|
||||
}
|
||||
|
||||
func UnmarshalNASAuthToken(token string) (err error, gamecd string, issuetime time.Time, userid uint64, gsbrcd string, cfc uint64, region byte, lang byte, ingamesn string, challenge string, unitcd byte, isLocalhost bool) {
|
||||
func UnmarshalNASAuthToken(token string) (gamecd string, issuetime time.Time, userid uint64, gsbrcd string, cfc uint64, region byte, lang byte, ingamesn string, challenge string, unitcd byte, isLocalhost bool, err error) {
|
||||
err = nil
|
||||
|
||||
if !strings.HasPrefix(token, "NDS") {
|
||||
err = errors.New("invalid auth token prefix")
|
||||
return
|
||||
|
|
@ -140,7 +142,9 @@ func MarshalGPCMLoginTicket(profileId uint32) string {
|
|||
return Base64DwcEncoding.EncodeToString(blob)
|
||||
}
|
||||
|
||||
func UnmarshalGPCMLoginTicket(ticket string) (err error, profileId uint32, issuetime time.Time) {
|
||||
func UnmarshalGPCMLoginTicket(ticket string) (profileId uint32, issuetime time.Time, err error) {
|
||||
err = nil
|
||||
|
||||
blob, err := Base64DwcEncoding.DecodeString(ticket)
|
||||
if err != nil {
|
||||
return
|
||||
|
|
|
|||
|
|
@ -13,12 +13,12 @@ type GameSpyCommand struct {
|
|||
}
|
||||
|
||||
var (
|
||||
InvalidGameSpyCommand = errors.New("invalid GameSpy command received")
|
||||
ErrInvalidGameSpyCommand = errors.New("invalid GameSpy command received")
|
||||
)
|
||||
|
||||
func ParseGameSpyMessage(msg string) ([]GameSpyCommand, error) {
|
||||
if !strings.Contains(msg, `\final\`) {
|
||||
return nil, InvalidGameSpyCommand
|
||||
return nil, ErrInvalidGameSpyCommand
|
||||
}
|
||||
|
||||
var commands []GameSpyCommand
|
||||
|
|
@ -31,7 +31,7 @@ func ParseGameSpyMessage(msg string) ([]GameSpyCommand, error) {
|
|||
for len(msg) > 0 && string(msg[0]) == `\` {
|
||||
keyEnd := strings.Index(msg[1:], `\`) + 1
|
||||
if keyEnd < 2 {
|
||||
return nil, InvalidGameSpyCommand
|
||||
return nil, ErrInvalidGameSpyCommand
|
||||
}
|
||||
|
||||
key := msg[1:keyEnd]
|
||||
|
|
|
|||
|
|
@ -689,8 +689,6 @@ func EncodeMatchCommand(command byte, data MatchCommandData) ([]byte, bool) {
|
|||
logging.Info("Common", "Unknown match command:", aurora.Cyan(command), "data:", data.Other)
|
||||
return data.Other, true
|
||||
}
|
||||
|
||||
return []byte{}, false
|
||||
}
|
||||
|
||||
func LogMatchCommand(moduleName string, dest string, command byte, data MatchCommandData) {
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ func (g *GameStatsSession) authp(command common.GameSpyCommand) {
|
|||
return
|
||||
}
|
||||
|
||||
err, _, issueTime, userId, gsbrcd, _, _, _, _, _, _, _ := common.UnmarshalNASAuthToken(authToken)
|
||||
_, issueTime, userId, gsbrcd, _, _, _, _, _, _, _, err := common.UnmarshalNASAuthToken(authToken)
|
||||
if err != nil {
|
||||
logging.Error(g.ModuleName, "Error unmarshalling authtoken:", err.Error())
|
||||
g.Write(errorCmd)
|
||||
|
|
|
|||
|
|
@ -66,10 +66,10 @@ func (g *GameSpySession) getAuthorizedFriendIndex(profileId uint32) int {
|
|||
}
|
||||
|
||||
const (
|
||||
addFriendMessage = "\r\n\r\n|signed|00000000000000000000000000000000"
|
||||
// addFriendMessage = "\r\n\r\n|signed|00000000000000000000000000000000"
|
||||
|
||||
// TODO: Check if this is needed for any game, it sends via bm 1
|
||||
authFriendMessage = "I have authorized your request to add me to your list"
|
||||
// authFriendMessage = "I have authorized your request to add me to your list"
|
||||
|
||||
logOutMessage = "|s|0|ss|Offline|ls||ip|0|p|0|qm|0"
|
||||
)
|
||||
|
|
@ -145,17 +145,6 @@ func (g *GameSpySession) addFriend(command common.GameSpyCommand) {
|
|||
g.exchangeFriendStatus(uint32(newProfileId))
|
||||
}
|
||||
|
||||
func (g *GameSpySession) sendFriendRequests() {
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
for _, newSession := range sessions {
|
||||
if newSession.isFriendAdded(g.User.ProfileId) {
|
||||
sendMessageToSessionBuffer("2", g.User.ProfileId, newSession, addFriendMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (g *GameSpySession) removeFriend(command common.GameSpyCommand) {
|
||||
strDelProfileID := command.OtherValues["delprofileid"]
|
||||
delProfileID64, err := strconv.ParseUint(strDelProfileID, 10, 32)
|
||||
|
|
@ -481,8 +470,7 @@ func (g *GameSpySession) bestieMessage(command common.GameSpyCommand) {
|
|||
} else if cmd == common.MatchResvOK || cmd == common.MatchResvDeny || cmd == common.MatchResvWait {
|
||||
if toSession.ReservationPID != g.User.ProfileId || toSession.Reservation.Reservation == nil {
|
||||
logging.Error(g.ModuleName, "Destination", aurora.Cyan(toProfileId), "has no reservation with the sender")
|
||||
g.replyError(ErrMessage)
|
||||
return
|
||||
// Allow the message through anyway to avoid a room deadlock
|
||||
}
|
||||
|
||||
if toSession.Reservation.Version != msgMatchData.Version {
|
||||
|
|
|
|||
|
|
@ -156,7 +156,7 @@ func (g *GameSpySession) login(command common.GameSpyCommand) {
|
|||
return
|
||||
}
|
||||
|
||||
err, gamecd, issueTime, userId, gsbrcd, cfc, region, lang, ingamesn, challenge, unitcd, isLocalhost := common.UnmarshalNASAuthToken(authToken)
|
||||
gamecd, issueTime, userId, gsbrcd, cfc, region, lang, ingamesn, challenge, unitcd, isLocalhost, err := common.UnmarshalNASAuthToken(authToken)
|
||||
if err != nil {
|
||||
g.replyError(ErrLogin)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -196,7 +196,7 @@ func handleRequest(conn net.Conn) {
|
|||
commands = session.handleCommand("wwfc_exlogin", commands, session.exLogin)
|
||||
commands = session.ignoreCommand("logout", commands)
|
||||
|
||||
if len(commands) != 0 && session.LoggedIn == false {
|
||||
if len(commands) != 0 && !session.LoggedIn {
|
||||
logging.Error(session.ModuleName, "Attempt to run command before login:", aurora.Cyan(commands[0]))
|
||||
session.replyError(ErrNotLoggedIn)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ func (g *GameSpySession) handleWWFCReport(command common.GameSpyCommand) {
|
|||
}
|
||||
|
||||
qr2.ProcessUSER(g.User.ProfileId, g.QR2IP, packet)
|
||||
break
|
||||
|
||||
case "mkw_malicious_packet":
|
||||
if g.GameName != "mariokartwii" {
|
||||
|
|
@ -47,7 +46,6 @@ func (g *GameSpySession) handleWWFCReport(command common.GameSpyCommand) {
|
|||
}
|
||||
|
||||
logging.Warn(g.ModuleName, "Malicious packet from", aurora.BrightCyan(strconv.FormatUint(profileId, 10)))
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import (
|
|||
"time"
|
||||
"wwfc/common"
|
||||
"wwfc/logging"
|
||||
"wwfc/qr2"
|
||||
|
||||
"github.com/logrusorgru/aurora/v3"
|
||||
)
|
||||
|
|
@ -186,78 +187,60 @@ func handleConnection(conn net.PacketConn, addr net.Addr, buffer []byte) {
|
|||
switch command {
|
||||
default:
|
||||
logging.Error(moduleName, "Received unknown command type:", aurora.Cyan(command))
|
||||
break
|
||||
|
||||
case NNInitRequest:
|
||||
// logging.Info(moduleName, "Command:", aurora.Yellow("NN_INIT"))
|
||||
session.handleInit(conn, addr, buffer[12:], moduleName, version)
|
||||
break
|
||||
|
||||
case NNInitReply:
|
||||
logging.Warn(moduleName, "Received server command:", aurora.Yellow("NN_INITACK"))
|
||||
break
|
||||
|
||||
case NNErtTestRequest:
|
||||
logging.Warn(moduleName, "Received server command:", aurora.Yellow("NN_ERTTEST"))
|
||||
break
|
||||
|
||||
case NNErtTestReply:
|
||||
logging.Info(moduleName, "Command:", aurora.Yellow("NN_ERTACK"))
|
||||
break
|
||||
|
||||
case NNStateUpdate:
|
||||
logging.Info(moduleName, "Command:", aurora.Yellow("NN_STATEUPDATE"))
|
||||
break
|
||||
|
||||
case NNConnectRequest:
|
||||
logging.Warn(moduleName, "Received server command:", aurora.Yellow("NN_CONNECT"))
|
||||
break
|
||||
|
||||
case NNConnectReply:
|
||||
// logging.Info(moduleName, "Command:", aurora.Yellow("NN_CONNECT_ACK"))
|
||||
session.handleConnectReply(conn, addr, buffer[12:], moduleName, version)
|
||||
break
|
||||
|
||||
case NNConnectPing:
|
||||
logging.Info(moduleName, "Command:", aurora.Yellow("NN_CONNECT_PING"))
|
||||
break
|
||||
|
||||
case NNBackupTestRequest:
|
||||
logging.Info(moduleName, "Command:", aurora.Yellow("NN_BACKUP_TEST"))
|
||||
break
|
||||
|
||||
case NNBackupTestReply:
|
||||
logging.Warn(moduleName, "Received server command:", aurora.Yellow("NN_BACKUP_ACK"))
|
||||
break
|
||||
|
||||
case NNAddressCheckRequest:
|
||||
logging.Info(moduleName, "Command:", aurora.Yellow("NN_ADDRESS_CHECK"))
|
||||
break
|
||||
|
||||
case NNAddressCheckReply:
|
||||
logging.Warn(moduleName, "Received server command:", aurora.Yellow("NN_ADDRESS_REPLY"))
|
||||
break
|
||||
|
||||
case NNNatifyRequest:
|
||||
logging.Info(moduleName, "Command:", aurora.Yellow("NN_NATIFY_REQUEST"))
|
||||
break
|
||||
|
||||
case NNReportRequest:
|
||||
// logging.Info(moduleName, "Command:", aurora.Yellow("NN_REPORT"))
|
||||
session.handleReport(conn, addr, buffer[12:], moduleName, version)
|
||||
break
|
||||
|
||||
case NNReportReply:
|
||||
logging.Warn(moduleName, "Received server command:", aurora.Yellow("NN_REPORT_ACK"))
|
||||
break
|
||||
|
||||
case NNPreInitRequest:
|
||||
logging.Info(moduleName, "Command:", aurora.Yellow("NN_PREINIT"))
|
||||
break
|
||||
|
||||
case NNPreInitReply:
|
||||
logging.Warn(moduleName, "Received server command:", aurora.Yellow("NN_PREINIT_ACK"))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -471,8 +454,11 @@ func (session *NATNEGSession) handleReport(conn net.PacketConn, addr net.Addr, b
|
|||
|
||||
if client, exists := session.Clients[clientIndex]; exists {
|
||||
client.Connected[client.ConnectingIndex] = true
|
||||
connecting := session.Clients[client.ConnectingIndex]
|
||||
client.ConnectingIndex = clientIndex
|
||||
client.ConnectAck = false
|
||||
|
||||
qr2.ProcessNATNEGReport(result, client.ServerIP, connecting.ServerIP)
|
||||
}
|
||||
|
||||
// Send remaining requests
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"net"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"wwfc/common"
|
||||
)
|
||||
|
||||
|
|
@ -40,5 +41,20 @@ func sendChallenge(conn net.PacketConn, addr net.Addr, session Session, lookupAd
|
|||
response = append(response, []byte(challenge)...)
|
||||
response = append(response, 0)
|
||||
|
||||
conn.WriteTo(response, addr)
|
||||
go func() {
|
||||
for {
|
||||
conn.WriteTo(response, addr)
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
|
||||
mutex.Lock()
|
||||
session, ok := sessions[lookupAddr]
|
||||
if !ok || session.Authenticated || session.LastKeepAlive < time.Now().Unix()-60 {
|
||||
mutex.Unlock()
|
||||
return
|
||||
}
|
||||
addr = session.Addr
|
||||
mutex.Unlock()
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
|
|
|||
35
qr2/group.go
35
qr2/group.go
|
|
@ -258,6 +258,41 @@ func CheckGPReservationAllowed(senderIP uint64, senderPid uint32, destPid uint32
|
|||
return checkReservationAllowed(moduleName, from, to, joinType)
|
||||
}
|
||||
|
||||
func ProcessNATNEGReport(result byte, ip1 string, ip2 string) {
|
||||
moduleName := "QR2:NATNEGReport"
|
||||
|
||||
ip1Lookup := makeLookupAddr(ip1)
|
||||
ip2Lookup := makeLookupAddr(ip2)
|
||||
|
||||
mutex.Lock()
|
||||
defer mutex.Unlock()
|
||||
|
||||
session1 := sessions[ip1Lookup]
|
||||
if session1 == nil {
|
||||
logging.Warn(moduleName, "Received NATNEG report for non-existent IP", aurora.Cyan(ip1))
|
||||
return
|
||||
}
|
||||
|
||||
session2 := sessions[ip2Lookup]
|
||||
if session2 == nil {
|
||||
logging.Warn(moduleName, "Received NATNEG report for non-existent IP", aurora.Cyan(ip2))
|
||||
return
|
||||
}
|
||||
|
||||
if session1.GroupPointer == nil || session1.GroupPointer != session2.GroupPointer {
|
||||
logging.Warn(moduleName, "Received NATNEG report for two IPs in different groups")
|
||||
return
|
||||
}
|
||||
|
||||
resultString := "2"
|
||||
if result == 1 {
|
||||
resultString = "1"
|
||||
}
|
||||
|
||||
session1.Data["+conn_"+session2.Data["+joinindex"]] = resultString
|
||||
session2.Data["+conn_"+session1.Data["+joinindex"]] = resultString
|
||||
}
|
||||
|
||||
func ProcessUSER(senderPid uint32, senderIP uint64, packet []byte) {
|
||||
moduleName := "QR2:ProcessUSER/" + strconv.FormatUint(uint64(senderPid), 10)
|
||||
|
||||
|
|
|
|||
|
|
@ -79,7 +79,6 @@ func handleConnection(conn net.PacketConn, addr net.Addr, buffer []byte) {
|
|||
switch packetType {
|
||||
case QueryRequest:
|
||||
logging.Info(moduleName, "Command:", aurora.Yellow("QUERY"))
|
||||
break
|
||||
|
||||
case ChallengeRequest:
|
||||
logging.Info(moduleName, "Command:", aurora.Yellow("CHALLENGE"))
|
||||
|
|
@ -94,24 +93,19 @@ func handleConnection(conn net.PacketConn, addr net.Addr, buffer []byte) {
|
|||
} else {
|
||||
mutex.Unlock()
|
||||
}
|
||||
break
|
||||
|
||||
case EchoRequest:
|
||||
logging.Info(moduleName, "Command:", aurora.Yellow("ECHO"))
|
||||
break
|
||||
|
||||
case HeartbeatRequest:
|
||||
// logging.Info(moduleName, "Command:", aurora.Yellow("HEARTBEAT"))
|
||||
heartbeat(moduleName, conn, addr, buffer)
|
||||
break
|
||||
|
||||
case AddErrorRequest:
|
||||
logging.Info(moduleName, "Command:", aurora.Yellow("ADDERROR"))
|
||||
break
|
||||
|
||||
case EchoResponseRequest:
|
||||
logging.Info(moduleName, "Command:", aurora.Yellow("ECHO_RESPONSE"))
|
||||
break
|
||||
|
||||
case ClientMessageRequest:
|
||||
logging.Info(moduleName, "Command:", aurora.Yellow("CLIENT_MESSAGE"))
|
||||
|
|
@ -144,7 +138,6 @@ func handleConnection(conn net.PacketConn, addr net.Addr, buffer []byte) {
|
|||
|
||||
case ClientRegisteredReply:
|
||||
logging.Info(moduleName, "Command:", aurora.Yellow("CLIENT_REGISTERED"))
|
||||
break
|
||||
|
||||
case ClientExploitReply:
|
||||
logging.Info(moduleName, "Command:", aurora.Yellow("CLIENT_EXPLOIT_ACK"))
|
||||
|
|
@ -153,7 +146,6 @@ func handleConnection(conn net.PacketConn, addr net.Addr, buffer []byte) {
|
|||
if login := session.Login; login != nil {
|
||||
login.NeedsExploit = false
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
logging.Error(moduleName, "Unknown command:", aurora.Yellow(buffer[0]))
|
||||
|
|
|
|||
|
|
@ -260,7 +260,9 @@ func SendClientMessage(senderIP string, destSearchID uint64, message []byte) {
|
|||
switch s.Fetch(true) {
|
||||
case &timeWaker:
|
||||
timeOutCount++
|
||||
if timeOutCount <= 8 {
|
||||
|
||||
// Enforce a 10 second timeout
|
||||
if timeOutCount <= 10 {
|
||||
break
|
||||
}
|
||||
|
||||
|
|
@ -332,8 +334,11 @@ func processClientMessage(moduleName string, sender, receiver *Session, message
|
|||
sender.ReservationID = receiver.SearchID
|
||||
} else if cmd == common.MatchResvOK || cmd == common.MatchResvDeny || cmd == common.MatchResvWait {
|
||||
if receiver.ReservationID != sender.SearchID || receiver.Reservation.Reservation == nil {
|
||||
logging.Error(moduleName, "Destination has no reservation with the sender")
|
||||
return
|
||||
logging.Warn(moduleName, "Destination has no reservation with the sender")
|
||||
if receiver.GroupPointer == nil || receiver.GroupPointer != sender.GroupPointer {
|
||||
return
|
||||
}
|
||||
// Allow the message through anyway to avoid a room deadlock
|
||||
}
|
||||
|
||||
if receiver.Reservation.Version != matchData.Version {
|
||||
|
|
|
|||
|
|
@ -52,6 +52,8 @@ func removeSession(addr uint64) {
|
|||
return
|
||||
}
|
||||
|
||||
session.MessageAckWaker.Assert()
|
||||
|
||||
if session.GroupPointer != nil {
|
||||
session.removeFromGroup()
|
||||
}
|
||||
|
|
@ -84,6 +86,16 @@ func (session *Session) removeFromGroup() {
|
|||
session.GroupPointer.findNewServer()
|
||||
}
|
||||
|
||||
for player := range session.GroupPointer.Players {
|
||||
delete(player.Data, "+conn_"+session.Data["+joinindex"])
|
||||
}
|
||||
|
||||
for field := range session.Data {
|
||||
if strings.HasPrefix(field, "+conn_") {
|
||||
delete(session.Data, field)
|
||||
}
|
||||
}
|
||||
|
||||
session.GroupPointer = nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -42,6 +42,5 @@ func HandleRequest(w http.ResponseWriter, r *http.Request) {
|
|||
case "/SakeStorageServer/StorageServer.asmx":
|
||||
moduleName := "SAKE:Storage:" + r.RemoteAddr
|
||||
handleStorageRequest(moduleName, w, r)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -147,19 +147,15 @@ func handleStorageRequest(moduleName string, w http.ResponseWriter, r *http.Requ
|
|||
switch xmlName {
|
||||
case SakeNamespace + "/GetMyRecords":
|
||||
response.Body.GetMyRecordsResponse = getMyRecords(moduleName, profileId, gameInfo, soap.Body.Data)
|
||||
break
|
||||
|
||||
case SakeNamespace + "/UpdateRecord":
|
||||
response.Body.UpdateRecordResponse = updateRecord(moduleName, profileId, gameInfo, soap.Body.Data)
|
||||
break
|
||||
|
||||
case SakeNamespace + "/SearchForRecords":
|
||||
response.Body.SearchForRecordsResponse = searchForRecords(moduleName, gameInfo, soap.Body.Data)
|
||||
break
|
||||
|
||||
default:
|
||||
logging.Error(moduleName, "Unknown SOAPAction:", aurora.Cyan(xmlName))
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
|
@ -190,7 +186,7 @@ func getRequestIdentity(moduleName string, request StorageRequestData) (uint32,
|
|||
return 0, common.GameInfo{}, false
|
||||
}
|
||||
|
||||
err, profileId, _ := common.UnmarshalGPCMLoginTicket(request.LoginTicket)
|
||||
profileId, _, err := common.UnmarshalGPCMLoginTicket(request.LoginTicket)
|
||||
if err != nil {
|
||||
logging.Error(moduleName, err)
|
||||
return 0, common.GameInfo{}, false
|
||||
|
|
@ -237,7 +233,7 @@ func getMyRecords(moduleName string, profileId uint32, gameInfo common.GameInfo,
|
|||
GetMyRecordsResult: "Error",
|
||||
}
|
||||
|
||||
values := map[string]StorageValue{}
|
||||
var values map[string]StorageValue
|
||||
|
||||
switch gameInfo.Name + "/" + request.TableID {
|
||||
default:
|
||||
|
|
@ -254,7 +250,6 @@ func getMyRecords(moduleName string, profileId uint32, gameInfo common.GameInfo,
|
|||
"recordid": intValue(int32(profileId)),
|
||||
"info": binaryDataValueBase64(database.GetMKWFriendInfo(pool, ctx, profileId)),
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
response := StorageGetMyRecordsResponse{
|
||||
|
|
@ -299,7 +294,6 @@ func updateRecord(moduleName string, profileId uint32, gameInfo common.GameInfo,
|
|||
// TODO: Validate record data
|
||||
database.UpdateMKWFriendInfo(pool, ctx, profileId, request.Values.RecordFields[0].Value.Value.Value)
|
||||
logging.Notice(moduleName, "Updated Mario Kart Wii friend info")
|
||||
break
|
||||
}
|
||||
|
||||
return &StorageUpdateRecordResponse{
|
||||
|
|
@ -343,14 +337,13 @@ func searchForRecords(moduleName string, gameInfo common.GameInfo, request Stora
|
|||
"info": binaryDataValueBase64(database.GetMKWFriendInfo(pool, ctx, uint32(ownerId))),
|
||||
},
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
// Sort the values now
|
||||
sort.Slice(values, func(l, r int) bool {
|
||||
lVal, lExists := values[l][request.Sort]
|
||||
rVal, rExists := values[r][request.Sort]
|
||||
if lExists == false || rExists == false {
|
||||
if !lExists || !rExists {
|
||||
// Prioritises the one that exists or goes left if both false
|
||||
return rExists
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user