Skip to content

TeamRealB/Wplace-Protocol

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

26 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Wplace Protocol

Wplace的技术栈、协议及接口的分析。

免责声明:部分没有被引用的接口没有列出,因为随时有可能移除,如果有任何错误,请及时联系我。

目录:

概念与系统

大多数命名为主观命名,不代表和源码或其他wplace项目中命名一致

地图

关键字:Map / Canvas / World

地图指Wplace的整体画布。基于墨卡托投影(Mercator Projection / Web Mercator)渲染,地图采用OpenFreeMap的Liberty Style。地图包含2048x2048也就是4,194,304瓦片,瓦片在前端通过Canvas覆盖在地图之上。

地图中大部分现实中没有属地/有争议的位置,都被划分为了最近的陆地所属国家或地区的一部分,例如,北太平洋被划分到了美国的檀香山,南太平洋被划分到了澳大利亚的亚当斯敦。

地图的总像素数量为 4,194,304,000,000(约 4.1 trillion / 4.1 兆 / 4.1 万亿)。

瓦片

关键字:Tile / Chunk

瓦片是wplace渲染画布的最小单位。每个瓦片在服务端是一张1000×1000的PNG图像,包含1,000,000个像素。

瓦片对应的数据类型为Vec2i,即 xy

API中提到的相对坐标也就是从所在瓦片的0开始坐标。

计算对应经纬度

整个地图在横向与纵向的瓦片数量均为2048。通过这个即可计算出Zoom值:

int n = 2048; // 瓦片数量
int z = (int) (Math.log(n) / Math.log(2)); // 通过换底公式求出Zoom

经过这个公式计算,可以求出zoom约为11,随后即可使用下列算法计算经纬度:

double n = Math.pow(2.0, 11); // zoom 为 11
double lon = (x + 0.5) / n * 360.0 - 180.0;
double latRad = Math.atan(Math.sinh(Math.PI * (1 - 2 * (y + 0.5) / n)));
double lat = Math.toDegrees(latRad);

其中的lonlat即为经纬度的值

公式参考自:Slippy map tilenames

相关接口

颜色

关键字:Color / Palette

Wplace提供了64种颜色,前32种为免费颜色,后32种每个需要2,000Droplets解锁。

对于颜色是否已经解锁,前端通过位掩码检查 (Bitmask Check)来检查extraColorsBitmapextraColorsBitmap为前端获得用户资料接口返回的Json中的一个字段。

其检查逻辑为:

int extraColorsBitmap = 0;
int colorId = 63; // 需要检查的颜色ID
boolean unlocked;

if (colorId < 32) { // 跳过前32因为前32个颜色是免费的
    unlocked = true;
} else {
    int mask = 1 << (colorId - 32);
    unlocked = (extraColorsBitmap & mask) != 0;
}

免责声明:此代码为笔者根据Wplace中的混淆过的JS代码分析得出的Java代码,而非原始代码。

对于颜色代码,请检查附录

相关接口

旗帜

关键字:Flag

Wplace包含251种旗帜,购买旗帜之后可以让你在对应的地区绘制时候节省10%的像素,一个旗帜的价格为20,000Droplets。

对于旗帜是否解锁通过一个自定义的BitMap来实现,以下是这个BitMap的JS代码:

class Tt {
    constructor(e) {
        u(this, "bytes");
        this.bytes = e ?? new Uint8Array
    }
    set(e, a) {
        const n = Math.floor(e / 8),
            c = e % 8;
        if (n >= this.bytes.length) {
            const r = new Uint8Array(n + 1),
                i = r.length - this.bytes.length;
            for (let h = 0; h < this.bytes.length; h++) r[h + i] = this.bytes[h];
            this.bytes = r
        }
        const l = this.bytes.length - 1 - n;
        a ? this.bytes[l] = this.bytes[l] | 1 << c : this.bytes[l] = this.bytes[l] & ~(1 << c)
    }
    get(e) {
        const a = Math.floor(e / 8),
            n = e % 8,
            c = this.bytes.length;
        return a > c ? !1 : (this.bytes[c - 1 - a] & 1 << n) !== 0
    }
}

BitMap可读的Java代码参见附录

前端通过用户资料接口获得flagsBitmap字段之后,通过Base64解码为Bytes然后传入BitMap读取某个旗帜ID是否已解锁。

对于全部旗帜代码,请参考附录

相关接口

等级

关键字:Level

等级可以根据已绘制的像素计算

double totalPainted = 1; // 已经绘制的像素数量
double base = Math.pow(30, 0.65);
double level = Math.pow(totalPainted, 0.65) / base;

每升一级会获得500droplets和增加2最大像素

商店

关键字:Store / Purchase

商店可以通过游戏内的虚拟货币 Droplet 购买物品,以下是物品列表

物品ID 物品名字 价格(Droplet) Variants
70 +5 Max. Charges 500
80 +30 Paint Charges 500
100 解锁付费颜色 2000 颜色ID
110 解锁旗帜 20000 旗帜ID

相关接口

其他物品ID预留给了充值物品(现金支付)

协议

如无特殊说明,URL主机为backend.wplace.live

对于常见的API错误,参阅附录

认证

认证通过Cookie中的字段j实现,在登录之后,后端会将Json Web Token保存到Cookie中,后续请求wplace.livebackend.wplace.live都会携带这个Cookie

Token是一段被编码的文本,而不是一个普通的随机字符串,可以通过jwt.io或任何JWT工具解码得到一些信息。

{
  "userId": 1,
  "sessionId": "",
  "iss": "wplace",
  "exp": 1758373929,
  "iat": 1755781929
}

其中exp字段为过期时间戳,可以仅通过token得出过期时间。

Cookie

通常来说请求接口只需要携带j一个Cookie即可,但是如果服务器处于高负载,开发者会开启Under Attack模式,如果开启Under Attack模式需要额外携带一个有效的cf_clearanceCookie,否则会弹出Cloudflare质询。

需要确保你在让自动程序发起请求时请求头中的大部分字段(如 User-AgentAccept-Language 等)和你获得cf_clearance的浏览器一致,否则会验证不通过仍然会弹出质询。

GET /me

获得用户信息

请求

  • 需要j完成认证

成功请求时的返回

{
    // int: Alliance ID
    "allianceId": 1, 
    // enum: Alliance 权限
    // admin/member
    "allianceRole": "admin",
    // boolean: 是否被封禁
    "banned": false,
    // object: 像素信息
    "charges": {
        // int: 恢复像素的间隔,单位为毫秒,30000毫秒也就是30秒
        "cooldownMs": 30000,
        // float: 剩余的像素
        "count": 35.821833333333586,
        // float: 最高像素数量
        "max": 500
    },
    // string: ISO-3166-1 alpha-2地区代码
    // 参考:https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2
    "country": "JP",
    // string: discord用户名
    "discord": "",
    // int: 剩余droplets
    "droplets": 75,
    // int: 装备的旗帜
    "equippedFlag": 0,
    // object: 灰度测试标记,其内部的意义不明确
    // 例如其中的variant值为koala(考拉),不明确其内部意义,仅为一个代号。
    // 但是会跟着请求头传出去,如果2025-09_pawtect的variant是disabled则不会发送pawtect-token
    // 说明部分用户没有被启用新的安全机制
    "experiments": {
        "2025-09_pawtect": {
            "variant": "koala"
        }
    },
    // int: extraColorsBitmap,参阅 #颜色 小节了解其作用。
    "extraColorsBitmap": 0,
    // array: 收藏的位置
    "favoriteLocations": [
        {
            "id": 1,
            "name": "",
            "latitude": 46.797833514893085,
            "longitude": 0.9266305280273432
        }
    ],
    // string: 已解锁的旗帜列表,参阅 #旗帜 小节了解其作用。
    "flagsBitmap": "AA==",
    // enum: 一般不会出现,如果你有权限才会额外显示
    // moderator/global_moderator/admin
    "role": "",
    // int: 用户ID
    "id": 1,
    // boolean: 是否有购买,如果有则会在菜单显示订单列表
    "isCustomer": false,
    // float: 等级
    "level": 94.08496005353335,
    // int: 最大的收藏数量,默认为15,暂时没有发现如何提升
    "maxFavoriteLocations": 15,
    // string: 用户名
    "name": "username",
    // boolean: 是否需要手机号验证,如果是则会在访问时弹出手机号验证窗口
    "needsPhoneVerification": false,
    // string: 头像URL或base64,需要根据前缀判断(例如data:image/png;base64,)
    "picture": "",
    // int: 已经绘制的像素数量
    "pixelsPainted": 114514,
    // boolean: 是否在alliance页面展示你最后一次绘制的位置
    "showLastPixel": true,
    // string: 你的解除封禁时间戳,如果是1970年则意味着没有被封禁或者已经被永久封禁。
    "timeoutUntil": "1970-01-01T00:00:00Z"
}

POST /me/update

更新当前用户的个人信息

请求

  • 需要 j 完成认证

请求示例

{
    // string:用户昵称
    "name": "cubk",
    // boolean:是否在alliance展示最后一个像素
    "showLastPixel": true,
    // discord用户名
    "discord": "_cubk"
}

成功返回

{
    "success": true
}

错误返回

{
    "error": "The name has more than 16 characters",
    "status": 400
}

请求体不合法

GET /me/profile-pictures

获得头像列表

一个人可以有多个头像(添加一个需要20,000Droplets),然后可以随时换头像列表中的任何一个头像

请求

  • 需要 j 完成认证

成功返回

// array: 所有头像
[
    {
        // int: 头像ID
        "id": 0,
        // string: 头像URL或者Base64,可以通过是否以data:image/png;base64,开头判断
        "url": ""
    }
]

如果你没有任何头像则会返回空的

POST /me/profile-picture/change

更换头像

请求

  • 需要 j 完成认证

示例请求

更换已有自定义头像

{
    // int: 头像ID,需要确保你添加了这个头像
	"pictureId": 1
}

重置头像

{}

请求空的JsonObject可以重置头像

成功返回

{
	"success": true
}

POST /me/profile-picture

上传头像

请求

  • 需要 j 完成认证
  • 请求体为Multipart File:image

成功返回

{
	"success": true
}

错误返回

{
	"error": "Forbidden",
	"status": 403
}

GET /alliance

获得Alliance信息

请求

  • 需要 j 完成认证

成功返回

{
	// string: Alliance介绍
	"description": "CCB",
	// object: 总部(Headquarters)
	"hq": {
		"latitude": 22.535013525851937,
		"longitude": 114.01152903098966
	},
	// int: Alliance ID
	"id": 453128,
	// int: 成员数量
	"members": 263,
	// string: 名字
	"name": "Team RealB",
	// string: 已绘制的总数
	"pixelsPainted": 1419281,
	// enum: 你的权限
	// admin/memeber
	"role": "admin"
}

错误返回

{
	"error": "Not Found",
	"status": 404
}

没有加入任何Alliance

POST /alliance

创建一个Alliance

请求

  • 需要 j 完成认证

请求示例

{
    // string: Alliance名字,不能重名。
	"name": "Team RealB"
}

成功返回

{
    // int: 创建完的Alliance ID
	"id": 1
}

错误返回

{
	"error": "name_taken",
	"status": 400
}

Alliance名字已经被占用

{
    "error": "Forbidden",
    "status": 403
}

已有一个Alliance但是仍然尝试创建,正常情况下不会触发。

POST /alliance/update-description

更新Alliance简介

请求

  • 需要 j 完成认证

示例请求

{
    "description": "bbb"
}

成功返回

{
	"success": true
}

错误返回

{
	"error": "Forbidden",
	"status": 403
}

没有Alliance或权限不是admin

GET /alliance/invites

获得Alliance的邀请链接

请求

  • 需要 j 完成认证

成功返回

// array: Alliance邀请链接,通常只有一个且格式为UUID
[
    "fe7c9c32-e95a-4f5f-a866-554cde2149c3"
]

错误返回

{
	"error": "Forbidden",
	"status": 403
}

没有Alliance或权限不是admin

GET /alliance/join/{invite}

通过Invite UUID加入Alliance,获得Invite UUID参阅/alliance/invites

请求

  • 需要 j 完成认证
  • URL中的{invite}参数为邀请UUID
    • 示例URL(设置为中国国旗):/alliance/join/fe7c9c32-e95a-4f5f-a866-554cde2149c3

成功返回

{
    "success": "true"
}

如果加入的目标和你已有的Alliance一致,也会返回成功

错误返回

{
    "error": "Not Found",
    "status": 404
}

没有找到目标Alliance

{
  "error": "Already Reported",
  "status": 208
}

已经加入了一个Alliance

{
	"error": "Forbidden",
	"status": 403
}

已被这个Alliance拉黑

POST /alliance/update-headquarters

更新Alliance的总部(Headquarters)

请求

  • 需要 j 完成认证

示例请求

{
	"latitude": 22.537655528880563,
	"longitude": 114.0274942853182
}

成功返回

{
	"success": true
}

错误返回

{
	"error": "Forbidden",
	"status": 403
}

没有Alliance或权限不是admin

GET /alliance/members/{page}

获得Alliance成员列表,有分页系统,有可能需要分多页获取如果成员超过50个

请求

  • 需要 j 完成认证
  • URL中的{page}参数为页码,从0开始
    • 示例URL(获得第一页):/alliance/members/0

成功返回

{
    // array: 一页最多50个
	"data": [{
	    // int: 用户ID
		"id": 1,
		// string: 用户名
		"name": "cubk'",
		// enum: 权限
		// admin/memeber
		"role": "admin"
	}, {
		"id": 1,
		"name": "SillyBitch",
		"role": "admin"
	}, {
		"id": 1,
		"name": "cubk",
		"role": "member"
	}],
	// boolean: 是否还有下一页
	"hasNext": true
}

错误返回

{
	"error": "Forbidden",
	"status": 403
}

没有Alliance或权限不是admin

GET /alliance/members/banned/{page}

获得Alliance已经拉黑的成员列表,有分页系统,有可能需要分多页获取如果成员超过50个

已经拉黑的成员无法再加入Alliance

请求

  • 需要 j 完成认证
  • URL中的{page}参数为页码,从0开始
    • 示例URL(获得第一页):/alliance/members/banned/0

成功返回

{
	"data": [{
		"id": 1,
		"name": "SuckMyDick"
	}],
	"hasNext": false
}

和普通成员接口大致一致,但是没有role,因为已经拉黑就不在alliance里了

错误返回

{
	"error": "Forbidden",
	"status": 403
}

没有Alliance或权限不是admin

POST /alliance/give-admin

将一个成员提升为Admin,无法降级

请求

  • 需要 j 完成认证

示例请求

{
    // int: 需要提升的用户ID
	"promotedUserId": 1
}

成功返回

本接口没有返回,响应码是200即成功

错误返回

{
	"error": "Forbidden",
	"status": 403
}

没有Alliance或权限不是admin

POST /alliance/ban

踢出并拉黑一个成员

拉黑之后如果不解除拉黑成员无法重新加入

请求

  • 需要 j 完成认证

示例请求

{
    // int: 需要踢出或拉黑的用户ID
	"bannedUserId": 1
}

成功返回

{
	"success": true
}

错误返回

{
	"error": "Forbidden",
	"status": 403
}

没有Alliance或权限不是admin

POST /alliance/unban

解除拉黑一个成员,解除之后他不会自动回到Alliance,只是可以重新加入了而已。

请求

  • 需要 j 完成认证

示例请求

{
    // int: 需要解除拉黑的用户ID
	"unbannedUserId": 1
}

成功返回

{
	"success": true
}

错误返回

{
	"error": "Forbidden",
	"status": 403
}

没有Alliance或权限不是admin

GET /alliance/leaderboard/{mode}

获得Alliance内玩家排行榜,仅限前50个。

请求

  • 需要 j 完成认证
  • URL中的mode代表时间范围,是一个枚举,可以是以下任何一个值:
    • today
    • week
    • month
    • all-time
  • 示例URL(今日排行榜):/alliance/leaderboard/today

成功返回

[
  {
    // int: 用户ID
    "userId": 10815100,
    // string: 用户名
    "name": "做爱",
    // int: 旗帜ID,旗帜列表参阅附录
    "equippedFlag": 0,
    // int: 已绘制像素数量
    "pixelsPainted": 32901,
    // 最后一次绘制的经纬度,如果用户关闭了showLastPixel则不会有这两个字段
    "lastLatitude": 22.527739206672393,
    "lastLongitude": 114.02762695312497
  },
  {
    "userId": 10850297,
    "name": "尹永铉",
    "equippedFlag": 0,
    "pixelsPainted": 31631
  }
]

POST /favorite-location

收藏一个位置

请求

  • 需要 j 完成认证

示例请求

{
	"latitude": 22.5199456234827,
	"longitude": 114.02428677802732
}

成功返回

{
    // int: 收藏ID
	"id": 1,
	"success": true
}

错误返回

{
  "error": "Forbidden",
  "status": 403
}

收藏数量超过maxFavoriteLocations

POST /favorite-location/delete

取消收藏位置

请求

  • 需要 j 完成认证

示例请求

{
    // int: 收藏ID
	"id": 1
}

成功返回

{
    "success": true
}

传入任何ID即使是没有收藏的或者不存在的也会返回成功

POST /purchase

购买物品,相关定义请阅读商店小节

请求

  • 需要 j 完成认证

示例请求

{
    // object: 固定字段product
	"product": {
	    // int: 物品id
		"id": 100,
		// int: 购买数量,对于Paint Charges/Max Charge可以购买多个
		"amount": 1,
		// int: 变体值,部分物品存在变体,如果没有变体不需要这个值
		"variant": 49
	}
}

成功返回

{
	"success": true
}

错误返回

所有错误在本接口返回的均一样

{"error":"Forbidden","status":403}{"success":true}

可能是巴西人毒品吃多了或者被足球精准命中后脑勺了导致大脑不太好使这里写错了但是这个响应体确实他妈的长这样,可能需要额外处理

proof

POST /flag/equip/{id}

设置展示旗帜

请求

  • 需要 j 完成认证
  • URL中的{id}参数为旗帜ID,所有旗帜ID和旗帜解锁检查参阅旗帜附录
    • 示例URL(设置为中国国旗):/flag/equip/45

成功返回

{
	"success": true
}

错误返回

{
	"error": "Forbidden",
	"status": 403
}

未解锁旗帜

GET /leaderboard/region/{mode}/{country}

获得某个国家/地区的地区绘制排行榜(仅前50个)

请求

  • URL中的mode代表时间范围,是一个枚举,可以是以下任何一个值:
    • today
    • week
    • month
    • all-time
  • URL中的country为地区ID,对应的表请参阅附录
  • 示例URL(中国今天的城市排行榜):/leaderboard/region/today/45

成功返回:

[
  {
    // int: 排行榜ID,仅用于内部
    "id": 111006,
    // int: 地区名字
    "name": "Yongzhou",
    // int: 地区ID
    "cityId": 4205,
    // int: 地区编号
    "number": 1,
    // int: 国家/地区ID
    "countryId": 45,
    // int: 已绘制数量
    "pixelsPainted": 389274,
    // 最后一次绘制的经纬度
    "lastLatitude": 26.59347856637528,
    "lastLongitude": 111.63313476562497
  },
  {
    "id": 112043,
    "name": "Fuzhou",
    "cityId": 4381,
    "number": 11,
    "countryId": 45,
    "pixelsPainted": 307461,
    "lastLatitude": 25.21710750136907,
    "lastLongitude": 120.43010742187496
  }
}

GET /leaderboard/country/{mode}

获得所有国家/地区排行榜,仅限前50个

请求

  • URL中的mode代表时间范围,是一个枚举,可以是以下任何一个值:
    • today
    • week
    • month
    • all-time
  • 示例URL(今天的国家地区排行榜):/leaderboard/country/today

成功返回

[
  {
    // int: 国家地区ID,参阅附录获得全部
    // 此处的235对应美国
    "id": 235,
    "pixelsPainted": 40724480
  },
  {
    "id": 181,
    "pixelsPainted": 39226725
  }
]

GET /leaderboard/player/{mode}

获得全球玩家排行榜,仅限前50个

请求

  • URL中的mode代表时间范围,是一个枚举,可以是以下任何一个值:
    • today
    • week
    • month
    • all-time
  • 示例URL(今天的玩家排行榜):/leaderboard/player/today

成功返回

[
  {
    // int: 用户ID
    "id": 8883244,
    // string: 用户名
    "name": "Tightmatt Cousin",
    // int: Alliance ID,如果是0则代表没有
    "allianceId": 0,
    // string: Alliance名字,如果没有则是空字符串
    "allianceName": "",
    // int: 已装备旗帜,旗帜列表参考附录,如果没有则是0
    "equippedFlag": 155,
    // int: 已绘制的像素数量
    "pixelsPainted": 64451,
    // string: 头像URL或Base64,可通过是否以data:image/png;base64,开头判断,如果没有头像则没有这个字段
    "picture": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAbklEQVR42qxTQQrAMAhbpN/e+/as7LKBjLRGOkGQ0mhM0zg2w2nAJ2XAAC8x7gpwVqCgi8zkvFhqAEEdKW2x6IoaxfSZqHjrYYhFcYfOM3IGythoGAeqHouJ33Mq1ihc13Vuq9k/sf2d7wAAAP//U48dVi53OIQAAAAASUVORK5CYII=",
    // string: discord用户名
    "discord": "co."
  },
  {
    "id": 2235271,
    "name": "( ˘ ³˘) ",
    "allianceId": 0,
    "allianceName": "",
    "equippedFlag": 0,
    "pixelsPainted": 39841,
    "discord": "bittenonce"
  }
]

GET /leaderboard/alliance/{mode}

获得全球Alliance排行榜,仅限前50个。

请求

  • URL中的mode代表时间范围,是一个枚举,可以是以下任何一个值:
    • today
    • week
    • month
    • all-time
  • 示例URL(今天的Alliance排行榜):/leaderboard/alliance/today

成功返回

[
  {
    // int: Alliance ID
    "id": 165,
    // string: Alliance名字
    "name": "bapo",
    // int: 已绘制像素数量
    "pixelsPainted": 771030
  },
  {
    "id": 29246,
    "name": "BROP Enterprises",
    "pixelsPainted": 507885
  }
]

GET /leaderboard/region/players/{city}/{mode}

获得某个城市的玩家排行榜,仅限前50个。

请求

  • URL中的mode代表时间范围,是一个枚举,可以是以下任何一个值:
    • today
    • week
    • month
    • all-time
  • URL中的city是城市ID,暂时没有一个明确的列表对应,因为城市太他妈多了。
  • 示例URL(深圳玩家总排行榜):/leaderboard/region/players/114594/all-time

成功返回

[
  {
    "id": 1997928,
    "name": "宵崎奏",
    "allianceId": 593067,
    "allianceName": "匠の心",
    "pixelsPainted": 189818,
    "equippedFlag": 98,
    "picture": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAA+ElEQVR42mJiQAP/ocBh8pP/GgHzwGwQDWOjq2dE1+w45SnDi727GCSc3VAUgsRg4MaGJLg+RpAmRkZGRphmQgDdICZkm7EpRtYAAiCXIbsOxWZkp2PzBjaXMDGQAbaJq8INJ8oAZG+ANCMDJnT/wfy90uAWmN6fI41hiNfL23CDmNBtAml8rsnIoFffDhe/vj4RLAaSR9YMNwBmCwhomumgaEQGIMORXUFyIMJcBdOM04APbQkExUDeASUkRvSEBJK4lMaGYcD1U1cYwi+owQMalpwZkfOBZuB8uAZQoIFpwywGt0nGDG9EkrDmBYoBE6UGAAIAAP//HhiiI4AXzBcAAAAASUVORK5CYII=",
    "discord": "思い出を取り戻して"
  },
  {
    "id": 7730493,
    "name": "$_0_U_/\\/\\_4",
    "allianceId": 597328,
    "allianceName": "義工",
    "pixelsPainted": 109076,
    "equippedFlag": 98,
    "picture": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAB+ElEQVR42pRTTWgTURD+9rmKBUPbg1RBDyslNAEriU3F3pTUS0FoQREEL3pQ8SwqRRREUPDkT3qwF6EIgvFUDyZ4EFKhG6om4IoElCpCBOmGoKiJrHwTJ27MJX7wmG/nzZv53sxbOygsBI1aAGJ9vwXlikrgY+mFJ/z4vomufaOOeyvPZZNJwmi4H3EsuVfWCn7gXxglDCj3/5QktEwoqlLbRAW/+/xvUkCL0BqtTuysbeiqEBsYbMk/Oy3c89dkaRLD7HpXtUxERQRlaw94MMy51iXizqWJREycN7OPUfbeYfW7DzMyhM8bf4l/ynGAT19x8doczk0fkriZeAJVtwLr9eIt6eJC9imOzuzHsDUgKvQ7NjkCuFVRwh4QVMGJUIGtjsPboxgvvpWAenqPHG4jNYSkW0Wk+FI+C0FEfMitwXBMqWYEoxfSclDvx8pUE05CLI9FZTLaYLNcetURVD9/sCWbY0pv6ZiI7mkjS7kyjJXcgf8FJzR76o4oMZwACR294vLDLBxnq7xSeUiF+UVx9IovH1bFuna9leD9YB+o5O6RG+2g+euPUHlwXxa5gjG7N20WfmL2tmXHp85YJw+MtX8xXuVK5rQ8XcXwH1u6mhc7ProLmWf5vz/T3JOipZ3l/DUwDC/3Bs3JqPDMUl7OkP8OAAD//6QS5QpYPtjuAAAAAElFTkSuQmCC",
    "discord": "soumasandesu"
  }
]

字段定义参阅/leaderboard/player/{mode}

GET /leaderboard/region/alliances/{city}/{mode}

获得某个城市的Alliance排行榜,仅限前50个。

请求

  • URL中的mode代表时间范围,是一个枚举,可以是以下任何一个值:
    • today
    • week
    • month
    • all-time
  • URL中的city是城市ID,暂时没有一个明确的列表对应,因为城市太他妈多了。
  • 示例URL(深圳Alliance总排行榜):/leaderboard/region/alliances/114594/all-time

成功返回

[
  {
    "id": 1,
    "name": "Team ReaIB",
    "pixelsPainted": 856069
  },
  {
    "id": 1,
    "name": "Team RealB",
    "pixelsPainted": 658302
  }
]

字段定义参阅/leaderboard/alliance/{mode}

GET /s0/tile/random

获得一个随机的已经绘制的像素

成功返回

{
    // 像素位置(相对于Tile)
	"pixel": {
		"x": 764,
		"y": 676
	},
	// Tile位置
	"tile": {
		"x": 1781,
		"y": 749
	}
}

Tile和像素位置之间的关系,参阅瓦片

GET /s0/pixel/{tileX}/{tileY}?x={x}&y={y}

获得某个像素点的信息

请求

  • URL中的tileX和tileY需要为瓦片坐标,相关信息参阅瓦片
  • x和y参数为像素相对坐标,需要在1024范围内
  • 示例URL(深圳的某个位置):/s0/pixel/1672/892?x=668&y=265

成功返回

已绘制

{
    // object: 绘制者信息
	"paintedBy": {
	    // int: 用户ID
		"id": 1,
		// string: 用户名
		"name": "崔龙海",
		// int: Alliance ID,如果没有则是0
		"allianceId": 1,
		// string: Alliance名字,如果没有则是空字符串
		"allianceName": "Team ReaIB",
		// int: 旗帜ID,对应关系参阅附录
		"equippedFlag": 0
	},
	// object: 区域信息
	"region": {
	    // int: 信息ID,内部使用
		"id": 114594,
		// int: 城市ID
		"cityId": 4263,
		// int: 城市名字
		"name": "Shenzhen",
		// int: 区域编号
		"number": 2,
		// int: 国家/地区ID
		"countryId": 45
	}
}

未绘制(透明)

{
	"paintedBy": {
		"id": 0,
		"name": "",
		"allianceId": 0,
		"allianceName": "",
		"equippedFlag": 0
	},
	"region": {
		"id": 114594,
		"cityId": 4263,
		"name": "Shenzhen",
		"number": 2,
		"countryId": 45
	}
}

GET /files/s0/tiles/{tileX}/{tileY}.png

获得某个瓦片的贴图

请求

  • URL中的tileX和tileY需要为瓦片坐标,相关信息参阅瓦片
  • 示例URL:/files/s0/tiles/1672/892.png

成功返回

ex

POST /s0/pixel/{tileX}/{tileY}

绘制像素

需要添加反作弊请求头x-pawtect-variantx-pawtect-token,请参阅反作弊

请求

  • 需要j完成认证
  • URL中的tileXtileY需要为瓦片坐标,相关信息参阅瓦片
  • 示例URL:/s0/pixel/1672/892

示例请求

{
    // array: 绘制的颜色ID,每个值对应一个像素
	"colors": [49, 49, 49, 49, 49, 49],
	// array: 绘制的坐标,格式为x, y, x, y,按照 (x, y) 成对出现
	// 坐标顺序与 colors 一一对应,即第N个颜色应用于第N个坐标
	"coords": [
      140, 359, 
      141, 359, 
      141, 358, 
      142, 358, 
      143, 358, 
      143, 357
    ],
    // string: 验证码token
	"t": "0.xxxx",
	// string: 浏览器指纹
	"fp": "xxxx"
}

colors为绘制的颜色代码和coords一一对应,参阅颜色附录

在绘制的颜色跨域多个瓦片时候会分多次请求

验证码token请参阅Turnstile fp请参阅浏览器指纹 x-pawtect-tokenx-pawtect-variant请参阅pawtect

成功返回

{
	"painted": 6
}

错误返回

{
	"error": "refresh",
	"status": 403
}

验证码token或pawtect无效

POST /report-user

举报用户,举报时客户端会渲染一张截图,客服在查看时可以看到客户端的截图和现场截图

客服可以看见被举报的用户的IP下的所有用户。

请求

  • 需要j完成认证
  • 请求体为multipart body
    • reportedUserId: 举报的用户ID
    • latitude: 纬度
    • longitude: 经度
    • zoom: 缩放
    • reason: 举报原因
    • notes: 举报文本,用户可以主动输入
    • image: 客户端渲染的一张举报截图会显示在客服页面

示例请求

CURL

curl -X POST "https://backend.wplace.live/report-user" \
  -H "Content-Type: multipart/form-data" \
  -F "reportedUserId=1" \
  -F "latitude=22.544484678446224" \
  -F "longitude=114.09375473639432" \
  -F "zoom=15.812584063490982" \
  -F "reason=griefing" \
  -F "notes=Messed up artworks for no reason" \
  -F "image=@图片;type=image/jpeg"

原始请求体

------boundary
Content-Disposition: form-data; name="reportedUserId"

1
------boundary
Content-Disposition: form-data; name="latitude"

22.544484678446224
------boundary
Content-Disposition: form-data; name="longitude"

114.09375473639432
------boundary
Content-Disposition: form-data; name="zoom"

15.812584063490982
------boundary
Content-Disposition: form-data; name="reason"

griefing
------boundary
Content-Disposition: form-data; name="notes"

Messed up artworks for no reason
------boundary
Content-Disposition: form-data; name="image"; filename="report-1758232933710.jpeg"
Content-Type: image/jpeg

(binary file data)
------boundary--

反作弊

对于/s0/pixel/{tileX}/{tileY}接口wplace添加了多个反作弊措施防止自动绘制和多账号。

lp - LocalStorage检测

在登录之后Local Storage会写入lp字段,是一个base64编码的json,解码之后可以看到

{
	"userId": 1,
	"time": 1758235291531
}

其中包含了你的用户ID和登录时间戳,当你尝试提交绘制但是用户ID和Local Storage不一致时会提示你请勿使用多个账号绘制

解决方案

  • 对于不跑在浏览器上的机器人或者脚本无视即可
  • 使用多个浏览器配置文件
  • 切换账号时候从Local Storage删除lp

Turnstile - 验证码

wplace使用了Turnstile验证码,并且每次绘制之后会在前端清除已经保存的验证码。

通常来说这个验证码不会频繁弹出,但是如果服务器处于高负载启动了Under Attack模式则会在每次绘制之前弹出。

Site Key为0x4AAAAAABpqJe8FO0N84q0F

解决方案

  • 打码平台付费自动通过验证码API
  • 通过中间人代理抓取到https://challenges.cloudflare.com中的cf-turnstile-response字段(在服务器没有开启Under Attack模式的情况下)
  • 自己打开一个浏览器挂脚本自动刷然后通过浏览器插件发回客户端。

FingerprintJS - 浏览器指纹

wplace使用FingerprintJS来上报visitorId(fp字段)来检测多账号和机器人。

也就是通过User-Agent, 屏幕分辨率, 时区等数据检测浏览器是不是无头、匿名模式等。

并且有0.001%的概率将你的信息卖给FingerprintJS的提供商。

function Q8() {
    if (!(window.__fpjs_d_m || Math.random() >= 0.001)) try {
        var _ = new XMLHttpRequest;
        _.open(
            'get',
            'https://m1.openfpcdn.io/fingerprintjs/v'.concat(I0, '/npm-monitoring'),
            !0
        ),
            _.send()
    } catch (s) {
        console.error(s)
    }
}

Wplace的JS中的真实代码,有0.001%的几率上传你的统计信息到FingerprintJS服务器

解决方案

  • 严格来说wplace暂时没有完全启用此检测因为只上传了一个visitorId(一个MD5值),理论上使用任何MD5都可以通过因为这个值无法从服务端校验,但是为了防止被检测到多账号建议使用MD5(userId + salt)

Pawtect

Pawtect是一个wplace最新最热引入的基于Rust编写的WASM模块,其样本可以在pawtect_wasm_bg.wasm查看,用于在请求之前对请求体进行签名,再通过请求头一同发送到服务器。

部分用户不会启用此检查,如果想知道某个账号是否启用了此检查,需要先请求/me获得其中的experiments信息,如果variant是disabled请求时候只需要传入x-pawtect-variant: disabled即可否则需要传入x-pawtect-variantx-pawtect-token两个请求头。

解决方案

  • 直接通过真实浏览器抓取(中间人代理或者浏览器插件)
  • 如果你使用Java开发可以使用本仓库的纯Java Pawtect实现:Pawtect.java(需要Bouncy Castle)
  • 通过下方参考代码加载WASM模块实现签名(如果你的脚本使用nodejs开发)

参考代码

let m;
let memory;
const textEncoder = new TextEncoder();
const textDecoder = new TextDecoder();
let J = 0;

function re(n, malloc, realloc) {
    if (realloc === undefined) {
        const s = textEncoder.encode(n);
        const ptr = malloc(s.length, 1) >>> 0;
        new Uint8Array(memory.buffer, ptr, s.length).set(s);
        J = s.length;
        return ptr;
    }
    let a = n.length;
    let ptr = malloc(a, 1) >>> 0;
    const mem = new Uint8Array(memory.buffer);
    let i = 0;
    for (; i < a; i++) {
        const code = n.charCodeAt(i);
        if (code > 0x7F) break;
        mem[ptr + i] = code;
    }
    if (i !== a) {
        if (i !== 0) n = n.slice(i);
        ptr = realloc(ptr, a, a = i + n.length * 3, 1) >>> 0;
        const view = new Uint8Array(memory.buffer, ptr + i, a - i);
        const { written } = textEncoder.encodeInto(n, view);
        i += written;
        ptr = realloc(ptr, a, i, 1) >>> 0;
    }
    J = i;
    return ptr;
}

function P(ptr, len) {
    return textDecoder.decode(new Uint8Array(memory.buffer, ptr, len));
}

function fn(n) {
    let e,
        t;
    try {
        const a = re(n, m.__wbindgen_malloc, m.__wbindgen_realloc),
            r = J,
            o = m.get_pawtected_endpoint_payload(a, r);
        return e = o[0],
            t = o[1],
            P(o[0], o[1])
    } finally {
        m.__wbindgen_free(e, t, 1)
    }
}

async function loadWASM() {
    const wasmBuffer = await readFile("./pawtect_wasm_bg.wasm");
    const imports = hn();
    const { instance } = await WebAssembly.instantiate(wasmBuffer, imports);
    m = instance.exports;
    memory = m.memory;
}

function hn() {
    const n = {};
    n.wbg = {};
    n.wbg.__wbg_buffer_609cc3eee51ed158 = e => e.buffer;
    n.wbg.__wbg_call_672a4d21634d4a24 = (e, t) => e.call(t);
    n.wbg.__wbg_call_7cccdd69e0791ae2 = (e, t, a) => e.call(t, a);
    n.wbg.__wbg_crypto_574e78ad8b13b65f = e => e.crypto;
    n.wbg.__wbg_getRandomValues_b8f5dbd5f3995a9e = (e, t) => e.getRandomValues(t);
    n.wbg.__wbg_msCrypto_a61aeb35a24c1329 = e => e.msCrypto;
    n.wbg.__wbg_new_a12002a7f91c75be = e => new Uint8Array(e);
    n.wbg.__wbg_newnoargs_105ed471475aaf50 = (e, t) => new Function(P(e, t));
    n.wbg.__wbg_newwithbyteoffsetandlength_d97e637ebe145a9a = (e, t, a) =>
        new Uint8Array(e, t >>> 0, a >>> 0);
    n.wbg.__wbg_newwithlength_a381634e90c276d4 = e => new Uint8Array(e >>> 0);
    n.wbg.__wbg_node_905d3e251edff8a2 = e => e.node;
    n.wbg.__wbg_process_dc0fbacc7c1c06f7 = e => e.process;
    n.wbg.__wbg_randomFillSync_ac0988aba3254290 = (e, t) => e.randomFillSync(t);
    n.wbg.__wbg_require_60cc747a6bc5215a = () => module.require;
    n.wbg.__wbg_set_65595bdd868b3009 = (e, t, a) => e.set(t, a >>> 0);
    n.wbg.__wbg_static_accessor_GLOBAL_88a902d13a557d07 = () =>
        typeof global === "undefined" ? null : global;
    n.wbg.__wbg_static_accessor_GLOBAL_THIS_56578be7e9f832b0 = () =>
        typeof globalThis === "undefined" ? null : globalThis;
    n.wbg.__wbg_static_accessor_SELF_37c5d418e4bf5819 = () =>
        typeof self === "undefined" ? null : self;
    n.wbg.__wbg_static_accessor_WINDOW_5de37043a91a9c40 = () =>
        typeof window === "undefined" ? null : window;
    n.wbg.__wbg_subarray_aa9065fa9dc5df96 = (e, t, a) => e.subarray(t >>> 0, a >>> 0);
    n.wbg.__wbg_versions_c01dfd4722a88165 = e => e.versions;
    n.wbg.__wbindgen_init_externref_table = () => {
        const e = m.__wbindgen_export_2;
        const t = e.grow(4);
        e.set(0, void 0);
        e.set(t + 0, void 0);
        e.set(t + 1, null);
        e.set(t + 2, true);
        e.set(t + 3, false);
    };
    n.wbg.__wbindgen_is_function = e => typeof e === "function";
    n.wbg.__wbindgen_is_object = e => typeof e === "object" && e !== null;
    n.wbg.__wbindgen_is_string = e => typeof e === "string";
    n.wbg.__wbindgen_is_undefined = e => e === void 0;
    n.wbg.__wbindgen_memory = () => m.memory;
    n.wbg.__wbindgen_string_new = (e, t) => P(e, t);
    n.wbg.__wbindgen_throw = (e, t) => {
        throw new Error(P(e, t));
    };
    return n;
}

// 需要自己添加post逻辑
// 示例传入:https://backend.wplace.live/s0/pixel/1/1, {}, 1
function postPaw(url, bodyStr, userId) {
    loadWASM();
    if (m.__wbindgen_start) m.__wbindgen_start();
    m.set_user_id(userId);
    const urlPtr = re(url, m.__wbindgen_malloc, m.__wbindgen_realloc);
    m.request_url(urlPtr, J);
    const loadPayload = m.get_load_payload();
    const sign = fn(bodyStr);
};

附录

通用API错误

{
  "error": "Unauthorized",
  "status": 401
}

未附带 j token 或者 token 无效

{
  "error": "Internal Server Error. We'll look into it, please try again later.",
  "status": 500
}

Cookie 已过期

{
  "error": "Bad Request",
  "status": 400
}

请求格式错误

全部颜色表

颜色 ID RGB 是否付费
0 透明 false
#000000 1 0, 0, 0 false
#3c3c3c 2 60, 60, 60 false
#787878 3 120, 120, 120 false
#d2d2d2 4 210, 210, 210 false
#ffffff 5 255, 255, 255 false
#600018 6 96, 0, 24 false
#ed1c24 7 237, 28, 36 false
#ff7f27 8 255, 127, 39 false
#f6aa09 9 246, 170, 9 false
#f9dd3b 10 249, 221, 59 false
#fffabc 11 255, 250, 188 false
#0eb968 12 14, 185, 104 false
#13e67b 13 19, 230, 123 false
#87ff5e 14 135, 255, 94 false
#0c816e 15 12, 129, 110 false
#10aea6 16 16, 174, 166 false
#13e1be 17 19, 225, 190 false
#28509e 18 40, 80, 158 false
#4093e4 19 64, 147, 228 false
#60f7f2 20 96, 247, 242 false
#6b50f6 21 107, 80, 246 false
#99b1fb 22 153, 177, 251 false
#780c99 23 120, 12, 153 false
#aa38b9 24 170, 56, 185 false
#e09ff9 25 224, 159, 249 false
#cb007a 26 203, 0, 122 false
#ec1f80 27 236, 31, 128 false
#f38da9 28 243, 141, 169 false
#684634 29 104, 70, 52 false
#95682a 30 149, 104, 42 false
#f8b277 31 248, 178, 119 false
#aaaaaa 32 170, 170, 170 true
#a50e1e 33 165, 14, 30 true
#fa8072 34 250, 128, 114 true
#e45c1a 35 228, 92, 26 true
#d6b594 36 214, 181, 148 true
#9c8431 37 156, 132, 49 true
#c5ad31 38 197, 173, 49 true
#e8d45f 39 232, 212, 95 true
#4a6b3a 40 74, 107, 58 true
#5a944a 41 90, 148, 74 true
#84c573 42 132, 197, 115 true
#0f799f 43 15, 121, 159 true
#bbfaf2 44 187, 250, 242 true
#7dc7ff 45 125, 199, 255 true
#4d31b8 46 77, 49, 184 true
#4a4284 47 74, 66, 132 true
#7a71c4 48 122, 113, 196 true
#b5aef1 49 181, 174, 241 true
#dba463 50 219, 164, 99 true
#d18051 51 209, 128, 81 true
#ffc5a5 52 255, 197, 165 true
#9b5249 53 155, 82, 73 true
#d18078 54 209, 128, 120 true
#fab6a4 55 250, 182, 164 true
#7b6352 56 123, 99, 82 true
#9c846b 57 156, 132, 107 true
#333941 58 51, 57, 65 true
#6d758d 59 109, 117, 141 true
#b3b9d1 60 179, 185, 209 true
#6d643f 61 109, 100, 63 true
#948c6b 62 148, 140, 107 true
#cdc59e 63 205, 197, 158 true

BitMap Java实现

public class WplaceBitMap {
    private byte[] bytes;

    public WplaceBitMap() {
        this.bytes = new byte[0];
    }

    public WplaceBitMap(byte[] bytes) {
        this.bytes = bytes != null ? bytes : new byte[0];
    }

    public void set(int index, boolean value) {
        int byteIndex = index / 8;
        int bitIndex = index % 8;

        if (byteIndex >= bytes.length) {
            byte[] newBytes = new byte[byteIndex + 1];
            int offset = newBytes.length - bytes.length;
            System.arraycopy(bytes, 0, newBytes, offset, bytes.length);
            bytes = newBytes;
        }

        int realIndex = bytes.length - 1 - byteIndex;

        if (value) {
            bytes[realIndex] |= (1 << bitIndex);
        } else {
            bytes[realIndex] &= ~(1 << bitIndex);
        }
    }

    public boolean get(int index) {
        int byteIndex = index / 8;
        int bitIndex = index % 8;

        if (byteIndex >= bytes.length) {
            return false;
        }

        int realIndex = bytes.length - 1 - byteIndex;
        return (bytes[realIndex] & (1 << bitIndex)) != 0;
    }

    public String toBase64() {
        return Base64.getEncoder().encodeToString(bytes);
    }
}

全部旗帜

旗帜 地区代码 ID
🇦🇫 AF 1
🇦🇱 AL 2
🇩🇿 DZ 3
🇦🇸 AS 4
🇦🇩 AD 5
🇦🇴 AO 6
🇦🇮 AI 7
🇦🇶 AQ 8
🇦🇬 AG 9
🇦🇷 AR 10
🇦🇲 AM 11
🇦🇼 AW 12
🇦🇺 AU 13
🇦🇹 AT 14
🇦🇿 AZ 15
🇧🇸 BS 16
🇧🇭 BH 17
🇧🇩 BD 18
🇧🇧 BB 19
🇧🇾 BY 20
🇧🇪 BE 21
🇧🇿 BZ 22
🇧🇯 BJ 23
🇧🇲 BM 24
🇧🇹 BT 25
🇧🇴 BO 26
🇧🇶 BQ 27
🇧🇦 BA 28
🇧🇼 BW 29
🇧🇻 BV 30
🇧🇷 BR 31
🇮🇴 IO 32
🇧🇳 BN 33
🇧🇬 BG 34
🇧🇫 BF 35
🇧🇮 BI 36
🇨🇻 CV 37
🇰🇭 KH 38
🇨🇲 CM 39
🇨🇦 CA 40
🇰🇾 KY 41
🇨🇫 CF 42
🇹🇩 TD 43
🇨🇱 CL 44
🇨🇳 CN 45
🇨🇽 CX 46
🇨🇨 CC 47
🇨🇴 CO 48
🇰🇲 KM 49
🇨🇬 CG 50
🇨🇰 CK 51
🇨🇷 CR 52
🇭🇷 HR 53
🇨🇺 CU 54
🇨🇼 CW 55
🇨🇾 CY 56
🇨🇿 CZ 57
🇨🇮 CI 58
🇩🇰 DK 59
🇩🇯 DJ 60
🇩🇲 DM 61
🇩🇴 DO 62
🇪🇨 EC 63
🇪🇬 EG 64
🇸🇻 SV 65
🇬🇶 GQ 66
🇪🇷 ER 67
🇪🇪 EE 68
🇸🇿 SZ 69
🇪🇹 ET 70
🇫🇰 FK 71
🇫🇴 FO 72
🇫🇯 FJ 73
🇫🇮 FI 74
🇫🇷 FR 75
🇬🇫 GF 76
🇵🇫 PF 77
🇹🇫 TF 78
🇬🇦 GA 79
🇬🇲 GM 80
🇬🇪 GE 81
🇩🇪 DE 82
🇬🇭 GH 83
🇬🇮 GI 84
🇬🇷 GR 85
🇬🇱 GL 86
🇬🇩 GD 87
🇬🇵 GP 88
🇬🇺 GU 89
🇬🇹 GT 90
🇬🇬 GG 91
🇬🇳 GN 92
🇬🇼 GW 93
🇬🇾 GY 94
🇭🇹 HT 95
🇭🇲 HM 96
🇭🇳 HN 97
🇭🇰 HK 98
🇭🇺 HU 99
🇮🇸 IS 100
🇮🇳 IN 101
🇮🇩 ID 102
🇮🇷 IR 103
🇮🇶 IQ 104
🇮🇪 IE 105
🇮🇲 IM 106
🇮🇱 IL 107
🇮🇹 IT 108
🇯🇲 JM 109
🇯🇵 JP 110
🇯🇪 JE 111
🇯🇴 JO 112
🇰🇿 KZ 113
🇰🇪 KE 114
🇰🇮 KI 115
🇽🇰 XK 116
🇰🇼 KW 117
🇰🇬 KG 118
🇱🇦 LA 119
🇱🇻 LV 120
🇱🇧 LB 121
🇱🇸 LS 122
🇱🇷 LR 123
🇱🇾 LY 124
🇱🇮 LI 125
🇱🇹 LT 126
🇱🇺 LU 127
🇲🇴 MO 128
🇲🇬 MG 129
🇲🇼 MW 130
🇲🇾 MY 131
🇲🇻 MV 132
🇲🇱 ML 133
🇲🇹 MT 134
🇲🇭 MH 135
🇲🇶 MQ 136
🇲🇷 MR 137
🇲🇺 MU 138
🇾🇹 YT 139
🇲🇽 MX 140
🇫🇲 FM 141
🇲🇩 MD 142
🇲🇨 MC 143
🇲🇳 MN 144
🇲🇪 ME 145
🇲🇸 MS 146
🇲🇦 MA 147
🇲🇿 MZ 148
🇲🇲 MM 149
🇳🇦 NA 150
🇳🇷 NR 151
🇳🇵 NP 152
🇳🇱 NL 153
🇳🇨 NC 154
🇳🇿 NZ 155
🇳🇮 NI 156
🇳🇪 NE 157
🇳🇬 NG 158
🇳🇺 NU 159
🇳🇫 NF 160
🇰🇵 KP 161
🇲🇰 MK 162
🇲🇵 MP 163
🇳🇴 NO 164
🇴🇲 OM 165
🇵🇰 PK 166
🇵🇼 PW 167
🇵🇸 PS 168
🇵🇦 PA 169
🇵🇬 PG 170
🇵🇾 PY 171
🇵🇪 PE 172
🇵🇭 PH 173
🇵🇳 PN 174
🇵🇱 PL 175
🇵🇹 PT 176
🇵🇷 PR 177
🇶🇦 QA 178
🇨🇩 CD 179
🇷🇴 RO 180
🇷🇺 RU 181
🇷🇼 RW 182
🇷🇪 RE 183
🇧🇱 BL 184
🇸🇭 SH 185
🇰🇳 KN 186
🇱🇨 LC 187
🇲🇫 MF 188
🇵🇲 PM 189
🇻🇨 VC 190
🇼🇸 WS 191
🇸🇲 SM 192
🇸🇹 ST 193
🇸🇦 SA 194
🇸🇳 SN 195
🇷🇸 RS 196
🇸🇨 SC 197
🇸🇱 SL 198
🇸🇬 SG 199
🇸🇽 SX 200
🇸🇰 SK 201
🇸🇮 SI 202
🇸🇧 SB 203
🇸🇴 SO 204
🇿🇦 ZA 205
🇬🇸 GS 206
🇰🇷 KR 207
🇸🇸 SS 208
🇪🇸 ES 209
🇱🇰 LK 210
🇸🇩 SD 211
🇸🇷 SR 212
🇸🇯 SJ 213
🇸🇪 SE 214
🇨🇭 CH 215
🇸🇾 SY 216
🇨🇳 TW 217
🇹🇯 TJ 218
🇹🇿 TZ 219
🇹🇭 TH 220
🇹🇱 TL 221
🇹🇬 TG 222
🇹🇰 TK 223
🇹🇴 TO 224
🇹🇹 TT 225
🇹🇳 TN 226
🇹🇲 TM 227
🇹🇨 TC 228
🇹🇻 TV 229
🇹🇷 TR 230
🇺🇬 UG 231
🇺🇦 UA 232
🇦🇪 AE 233
🇬🇧 GB 234
🇺🇸 US 235
🇺🇲 UM 236
🇺🇾 UY 237
🇺🇿 UZ 238
🇻🇺 VU 239
🇻🇦 VA 240
🇻🇪 VE 241
🇻🇳 VN 242
🇻🇬 VG 243
🇻🇮 VI 244
🇼🇫 WF 245
🇪🇭 EH 246
🇾🇪 YE 247
🇿🇲 ZM 248
🇿🇼 ZW 249
🇦🇽 AX 250
🇮🇨 IC 251

About

Analysis of Wplace's technology stack, protocols, and endpoints.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Contributors 3

  •  
  •  
  •