站内有两篇长文要先划清边界:一篇是游戏本地化与多语言指南 ,讲的是本地化行业与语言分工;另一篇是出海合规与税务风险清单 ,讲的是法务与税务口径。本节都不重写。它要回答的是一个更窄、也更工程的问题:手上这批素材,哪些地区能发、哪些不能发。这个问题之所以必须工程化,是因为它每天都在变——今天买的一首背景音乐,可能只授权北美与欧洲;一个看起来无害的名称,可能在巴西已被别人注册成商标。靠人脑记「哪张图能发哪国」,迟早出事。
前面第 6 章我们把文化禁忌做成了规则表,但禁忌只回答「这张图在文化上会不会冒犯」。本节补上另一半:这张图在法律上有没有授权。两者合起来,才是「一份内容能不能进某个市场」的完整判据。
版权不是一个状态,是一张地区矩阵
一个常见误判是把版权当成一个布尔值:「这图我们买了,能用」。真实的授权是有地域范围的。同一份素材,可能有三种档位:全球授权(allows 含 "*")、地区白名单(只在少数市场有效)、地区黑名单(全球可用,但个别市场被排除,通常是商标冲突或当地法规)。再叠上两个开关:是否要求署名(attributionRequired)和是否允许二次创作(derivativeAllowed)——后者直接决定这份素材能不能被裁成地区变体。
把这三个维度写成一条公式,就得到了本节的核心判据:
available(content, region) = ((region ∈ allows) 或 ("*" ∈ allows)) 且 (region ∉ denies)
denies 的优先级高于 allows:哪怕一份素材是「全球授权」,只要它在某地区的 denies 里,那个地区就不发。这条规则听起来简单,却能把「我们明明买了全球授权」这种想当然挡在门外。
三个维度:授权区间、署名、二创
登记表是纯数据,每份内容登记 kind(音乐 / 音效 / 美术 / 字体 / 名称 / 贴图 / 徽标 / 照片)、source(自制 / 委托 / 买断 / 授权 / 商标)、allows / denies 与两个开关:
{
"version": 1,
"game": "merge-island",
"observedAt": "2026-10",
"note": "内容与版权的地区差异登记表。口径是「公开可观察机制」的建模假设(观察时点 2026-10,非实测):把每份素材的授权来源、是否要求署名、是否允许二次创作,以及授权允许 / 拒绝发行的地区整理成可机读的登记表,不是任何版权方或平台的逐条法律结论,也不构成法律意见。allows 里的 \"*\" 表示全球授权;denies 优先级高于 allows。available(content, region) = ((region ∈ allows) 或 (\"*\" ∈ allows)) 且 (region ∉ denies)。真实授权合同与真实商标检索需真实业务环境,命令行不可复现(X)。全整数,无浮点。",
"regions": [
{ "id": "us", "name": "北美" },
{ "id": "eu", "name": "欧洲" },
{ "id": "cn", "name": "中国大陆" },
{ "id": "jp", "name": "日本" },
{ "id": "kr", "name": "韩国" },
{ "id": "tw", "name": "台港澳" },
{ "id": "br", "name": "巴西" },
{ "id": "in", "name": "印度" },
{ "id": "sea", "name": "东南亚" },
{ "id": "mena", "name": "中东" }
],
"contents": [
{ "id": "bgm_island", "name": "背景音乐·小岛", "kind": "music", "source": "stock-royalty-free", "attributionRequired": 1, "derivativeAllowed": 1, "allows": ["*"], "denies": [] },
{ "id": "sfx_merge", "name": "音效·合成", "kind": "sfx", "source": "self-made", "attributionRequired": 0, "derivativeAllowed": 1, "allows": ["*"], "denies": [] },
{ "id": "art_hero", "name": "主视觉·小岛", "kind": "art", "source": "commissioned", "attributionRequired": 0, "derivativeAllowed": 1, "allows": ["*"], "denies": [] },
{ "id": "font_cjk", "name": "字体·汉字", "kind": "font", "source": "licensed", "attributionRequired": 0, "derivativeAllowed": 0, "allows": ["cn", "jp", "kr", "tw"], "denies": [] },
{ "id": "name_island", "name": "名称·合成小岛", "kind": "name", "source": "trademark", "attributionRequired": 0, "derivativeAllowed": 0, "allows": ["*"], "denies": ["br"] },
{ "id": "stock_texture", "name": "贴图·沙滩", "kind": "texture", "source": "stock-royalty-free", "attributionRequired": 0, "derivativeAllowed": 1, "allows": ["us", "eu"], "denies": [] },
{ "id": "partner_logo", "name": "合作方 Logo", "kind": "logo", "source": "partner-license", "attributionRequired": 1, "derivativeAllowed": 0, "allows": ["*"], "denies": ["cn", "mena"] },
{ "id": "music_pop", "name": "授权流行曲", "kind": "music", "source": "licensed", "attributionRequired": 0, "derivativeAllowed": 0, "allows": ["us", "eu", "jp"], "denies": [] },
{ "id": "photo_beach", "name": "照片·海滩", "kind": "photo", "source": "stock-royalty-free", "attributionRequired": 0, "derivativeAllowed": 0, "allows": ["*"], "denies": [] }
]
}
D 层:把授权登记表扫成可用性矩阵
下面这段是 D 层校验脚本 sim/g12_content_rights.py。它读登记表,把「内容授权」与「发行地区」叉乘成一张可用性矩阵,统计可发对数、被阻断区、每个内容可发行的地区数,并单独列出死资产(授权区间与发行地区零交集,等于白买)与两个开关的清单:
#!/usr/bin/env python3
# file: sim/g12_content_rights.py
# tier: D
# verify: /opt/homebrew/bin/python3 sim/g12_content_rights.py --config config/ --out out/
"""内容与版权的地区可用性矩阵校验(D 层:清单结构 / lint 校验)。
读 config/g12_content_rights.json,把「内容授权」与「发行地区」做笛卡尔匹配,
得到一张 content × region 的可用性矩阵:
available(content, region) = ((region ∈ allows) 或 ("*" ∈ allows)) 且 (region ∉ denies)
availablePairs = available==1 的 (content, region) 对数
blockedPairs = available==0 的 (content, region) 对数
shipRegions(c) = 该内容可发行的地区
deadAssets = shipCount==0 的内容(授权区间与发行地区零交集,等于白买)
needsAttribution = attributionRequired==1 的内容(须在关于页署名)
noDerivative = derivativeAllowed==0 的内容(不可裁剪 / 改色,地区变体受限)
口径:授权登记表是「公开可观察机制」的建模假设,不是版权方的逐条法律结论,
也不构成法律意见。真实授权合同与商标检索需真实业务环境,命令行不可复现(X)。
产出 out/g12_content_rights.json + .csv + .svg。全整数,无浮点,无随机。
"""
from __future__ import annotations
import argparse
import csv
import json
import os
import xml.etree.ElementTree as ET
def is_available(content: dict, region: str) -> int:
"""内容在指定地区的可用性:allows 命中且不在 denies。denies 优先。"""
allows = content["allows"]
if region in content["denies"]:
return 0
return 1 if ("*" in allows or region in allows) else 0
def scan(cfg: dict):
regions = [r["id"] for r in cfg["regions"]]
pairs, content_rows = [], []
for c in cfg["contents"]:
ship = [reg for reg in regions if is_available(c, reg) == 1]
for reg in regions:
pairs.append({"content": c["id"], "region": reg, "available": is_available(c, reg)})
content_rows.append({"id": c["id"], "name": c["name"], "kind": c["kind"],
"source": c["source"], "attributionRequired": c["attributionRequired"],
"derivativeAllowed": c["derivativeAllowed"], "shipRegions": ship,
"shipCount": len(ship), "blockedCount": len(regions) - len(ship)})
return pairs, content_rows
def draw_svg(path: str, rows: list[dict], regions: list[str]) -> int:
"""纯 Python SVG:content × region 可用性热力格(绿=可发、红=不可发)。"""
CW, CH, LEFT, TOP = 52, 30, 220, 74
W = LEFT + CW * len(regions) + 20
H = TOP + CH * len(rows) + 20
svg = ET.Element("svg", {"xmlns": "http://www.w3.org/2000/svg", "width": str(W),
"height": str(H), "viewBox": f"0 0 {W} {H}"})
ET.SubElement(svg, "rect", {"width": str(W), "height": str(H), "fill": "#ffffff"})
ET.SubElement(svg, "text", {"x": "16", "y": "28", "font-size": "15",
"fill": "#111827"}).text = "内容 × 地区可用性矩阵(绿=可发 / 红=不可发)"
for k, reg in enumerate(regions):
ET.SubElement(svg, "text", {"x": str(LEFT + k * CW + 14), "y": str(TOP - 10),
"font-size": "11", "fill": "#6b7280"}).text = reg
for i, r in enumerate(rows):
y = TOP + i * CH
ET.SubElement(svg, "text", {"x": "16", "y": str(y + 19), "font-size": "11",
"fill": "#374151"}).text = f'{r["name"]}({r["shipCount"]}/{len(regions)})'
for k, reg in enumerate(regions):
avail = 1 if reg in r["shipRegions"] else 0
ET.SubElement(svg, "rect", {"x": str(LEFT + k * CW + 2), "y": str(y + 4),
"width": str(CW - 4), "height": str(CH - 8), "rx": "3",
"fill": "#16a34a" if avail else "#ef4444"})
os.makedirs(os.path.dirname(path), exist_ok=True)
ET.ElementTree(svg).write(path, encoding="utf-8", xml_declaration=True)
return len(ET.tostring(svg))
def main() -> int:
ap = argparse.ArgumentParser()
ap.add_argument("--config", default="config/")
ap.add_argument("--out", default="out/")
a = ap.parse_args()
with open(os.path.join(a.config, "g12_content_rights.json"), encoding="utf-8") as f:
cfg = json.load(f)
regions = [r["id"] for r in cfg["regions"]]
pairs, content_rows = scan(cfg)
available_pairs = sum(1 for p in pairs if p["available"] == 1)
blocked_pairs = len(pairs) - available_pairs
dead = [r["id"] for r in content_rows if r["shipCount"] == 0]
needs_attr = [r["id"] for r in content_rows if r["attributionRequired"] == 1]
no_deriv = [r["id"] for r in content_rows if r["derivativeAllowed"] == 0]
region_rows = [{"region": reg, "availableContents": sum(1 for p in pairs if p["region"] == reg and p["available"] == 1)}
for reg in regions]
digest = (f"contents={len(content_rows)},regions={len(regions)},available={available_pairs},"
f"blocked={blocked_pairs},needsAttribution={len(needs_attr)},noDerivative={len(no_deriv)}")
os.makedirs(a.out, exist_ok=True)
svg = draw_svg(os.path.join(a.out, "g12_content_rights.svg"), content_rows, regions)
with open(os.path.join(a.out, "g12_content_rights.csv"), "w", encoding="utf-8", newline="") as f:
w = csv.writer(f)
w.writerow(["content", "region", "available"])
for p in pairs:
w.writerow([p["content"], p["region"], p["available"]])
rep = {"version": cfg["version"], "game": cfg["game"], "observedAt": cfg["observedAt"],
"regionCount": len(regions), "contentCount": len(content_rows),
"availablePairs": available_pairs, "blockedPairs": blocked_pairs,
"deadAssets": dead, "needsAttribution": needs_attr, "noDerivative": no_deriv,
"contentSummary": content_rows, "regionSummary": region_rows,
"digest": digest, "svg": os.path.join(a.out, "g12_content_rights.svg"), "svgBytes": svg}
with open(os.path.join(a.out, "g12_content_rights.json"), "w", encoding="utf-8") as f:
json.dump(rep, f, ensure_ascii=False, indent=2)
print(json.dumps({"digest": digest, "availablePairs": available_pairs, "blockedPairs": blocked_pairs,
"deadAssets": dead, "needsAttribution": needs_attr, "noDerivative": no_deriv,
"contentSummary": [{"id": r["id"], "shipCount": r["shipCount"],
"blockedCount": r["blockedCount"]} for r in content_rows]},
ensure_ascii=False, indent=2))
return 0
if __name__ == "__main__":
raise SystemExit(main())
在本机跑 /opt/homebrew/bin/python3 sim/g12_content_rights.py --config config/ --out out/,9 份内容 × 10 个地区 = 90 对的结果是(同机相对量级,绝对数值不可移植):
digest = contents=9,regions=10,available=66,blocked=24,needsAttribution=2,noDerivative=5
90 对里 66 对可发、24 对被阻断。逐内容看(shipCount 是可发行地区数):
| 内容 | 授权来源 | 可发行 | 被阻断 | 读法 |
|---|---|---|---|---|
| 背景音乐·小岛 | 免版税素材 | 10 | 0 | 全球授权,但须署名 |
| 音效·合成 | 自制 | 10 | 0 | 干净,全区可发 |
| 主视觉·小岛 | 委托 | 10 | 0 | 干净,全区可发 |
| 字体·汉字 | 授权 | 4 | 6 | 只在 4 个 CJK 市场有效 |
| 名称·合成小岛 | 商标 | 9 | 1 | 巴西商标冲突 |
| 贴图·沙滩 | 免版税素材 | 2 | 8 | 只在欧美授权 |
| 合作方 Logo | 合作授权 | 8 | 2 | 中国大陆与中东被排除 |
| 授权流行曲 | 授权 | 3 | 7 | 只在美国 / 欧盟 / 日本 |
| 照片·海滩 | 免版税素材 | 10 | 0 | 全球授权,但禁止二创 |
这张表最有价值的一行是贴图·沙滩:它只有 2 个市场可发。它不是「某个地区要换图」,而是这张贴图在当前授权下只能进欧美——如果发行组合里有日韩,就必须换素材或补授权。另一个信号是 deadAssets 为空:没有哪份素材的授权区间与发行地区零交集(那种情况等于白买),说明当前素材池的授权都至少覆盖一个目标市场。
P 层内核:跨语言对拍,证明判定没写错
一张表看不出「是判定对」还是「碰巧看着对」。本套用一份 P 层 TypeScript 内核 core/src/g12/content_rights.ts 复算同一套匹配:同样的 denies 优先、同样的 "*" 展开、同样的地区计数。它读同一份 config/g12_content_rights.json,产出与 Python 逐字符一致的摘要。
// file: core/src/g12/content_rights.ts
// tier: P
// verify: node --test 'core/test/g12_*.test.ts'
//
// 内容与版权的地区可用性内核:与 sim/g12_content_rights.py 逐位等价的 TypeScript 对偶实现。
// 读同一份 config/g12_content_rights.json;全整数、无浮点、无随机、禁 Math.random。
// available(content, region) = ((region ∈ allows) 或 ("*" ∈ allows)) 且 (region ∉ denies)。
export interface RightsRegion { readonly id: string; readonly name: string }
export interface ContentRights {
readonly id: string; readonly name: string; readonly kind: string; readonly source: string;
readonly attributionRequired: number; readonly derivativeAllowed: number;
readonly allows: readonly string[]; readonly denies: readonly string[];
}
export interface RightsConfig {
readonly version: number; readonly game: string; readonly observedAt: string;
readonly regions: readonly RightsRegion[]; readonly contents: readonly ContentRights[];
}
export interface ContentRow {
readonly id: string; readonly name: string; readonly kind: string; readonly source: string;
readonly attributionRequired: number; readonly derivativeAllowed: number;
readonly shipRegions: readonly string[]; readonly shipCount: number; readonly blockedCount: number;
}
export interface ScanResult { readonly pairs: readonly { content: string; region: string; available: number }[];
readonly contents: readonly ContentRow[] }
/** 内容在指定地区的可用性:allows 命中且不在 denies;denies 优先。 */
export function isAvailable(content: ContentRights, region: string): number {
if (content.denies.includes(region)) return 0;
return content.allows.includes("*") || content.allows.includes(region) ? 1 : 0;
}
/** 扫描整张矩阵:逐内容 × 逐地区求可用性、可发行地区、被阻断地区。 */
export function scan(cfg: RightsConfig): ScanResult {
const pairs: { content: string; region: string; available: number }[] = [];
const contents: ContentRow[] = [];
for (const c of cfg.contents) {
const ship = cfg.regions.filter((r) => isAvailable(c, r.id) === 1).map((r) => r.id);
for (const r of cfg.regions) pairs.push({ content: c.id, region: r.id, available: isAvailable(c, r.id) });
contents.push({ id: c.id, name: c.name, kind: c.kind, source: c.source,
attributionRequired: c.attributionRequired, derivativeAllowed: c.derivativeAllowed,
shipRegions: ship, shipCount: ship.length, blockedCount: cfg.regions.length - ship.length });
}
return { pairs, contents };
}
/** 跨语言对拍摘要:与 sim/g12_content_rights.py 的 digest 逐字符一致。 */
export function digest(cfg: RightsConfig): string {
const { pairs, contents } = scan(cfg);
const available = pairs.filter((p) => p.available === 1).length;
const blocked = pairs.length - available;
const needsAttr = contents.filter((c) => c.attributionRequired === 1).length;
const noDeriv = contents.filter((c) => c.derivativeAllowed === 0).length;
return `contents=${contents.length},regions=${cfg.regions.length},available=${available},`
+ `blocked=${blocked},needsAttribution=${needsAttr},noDerivative=${noDeriv}`;
}
/** 授权区间与发行地区零交集的内容(白买),工程上应在上架前拦下。 */
export function deadAssets(cfg: RightsConfig): string[] {
return scan(cfg).contents.filter((c) => c.shipCount === 0).map((c) => c.id);
}
/** 某内容可发行的地区数。 */
export function shipCount(cfg: RightsConfig, contentId: string): number {
const c = cfg.contents.find((x) => x.id === contentId);
if (c === undefined) return 0;
return cfg.regions.filter((r) => isAvailable(c, r.id) === 1).length;
}
配套测试 core/test/g12_content_rights.test.ts 把 Python 落盘的摘要写成金标准,断言 TS 内核复算结果逐字符相等,并验 denies 优先、全球授权展开、地区受限与死资产。在本机跑 node --test 'core/test/g12_*.test.ts',本节 6 个用例全过:
✔ 跨语言一致:TS 可用性摘要 == Python 金标准
✔ 矩阵规模:9 内容 × 10 地区 = 90 对,可发 66 对
✔ denies 优先:名称在巴西被商标冲突挡下,其余 9 区可发
✔ 地区受限:字体只在 4 个 CJK 市场、贴图只在 2 个欧美市场可发
✔ 全球授权:自制音效在全部 10 个地区可发,无死资产
✔ 署名与二创:2 份素材须署名、5 份禁止二创(地区变体受限)
ℹ tests 6 / pass 6 / fail 0
只要有一边的 denies 优先级、"*" 展开或地区计数写错,摘要立刻分叉。这与第 6 章「文化禁忌与符号审查」
是同一个信念:同一张登记表,在 Python 校验器与 TS 内核里必须判得一模一样。而版权侧比文化侧更容易被忽视——文化禁忌是「会冒犯谁」,版权是「能不能合法用」,后者一旦出错是法律风险,不是体验问题。
X 层:真实授权与商标检索不可复现
必须把边界划清。上面算的是规则侧:给定一张授权登记表与一批地区,可用性矩阵是什么。而下面这些属于 X 层,需真实业务环境,命令行不可复现:
- 各地区的真实商标检索结果(同名商标是否存在、在哪个类别注册,需逐国检索,且状态随时变动);
- 各版权方的真实授权条款(
allows/denies是登记表的建模值,与合同原文的偏差靠法务复核兜住); - 各平台的真实版权审核结论(同一素材在不同平台的判定可能不同)。
工程上正确的做法是:把可复现的规则层与不可复现的授权层分开——规则层保证「买了哪档授权就只发哪些地区」这条纪律被自动执行;授权层靠合同与检索结果反标定登记表(补一份授权就改一行 allows,矩阵立刻更新)。这也是本节与「上架资料、审核与驳回」
的接口:版权可用性直接决定上架时能勾选哪些地区。至于「名称 / 商标该不该注册」这类判断,属于版号与资质办理
的口径,本节只做素材侧的工程落点。
自检表
| 检查项 | 合格标准 |
|---|---|
| 三维登记 | 授权区间、署名、二创三个维度都进表,不只看「买没买」 |
| denies 优先 | 黑名单高于白名单,全球授权也能被单点排除 |
| 死资产可查 | 授权区间与发行地区零交集的内容被单独列出 |
| 跨语言一致 | Python 校验器与 TS 内核摘要逐字符相同 |
| 边界清晰 | 真实商标 / 授权条款标 X;不写成「实测」,不提供法律意见 |
到这里,内容侧的地区差异就立起来了:先有授权矩阵,才谈得上多市场发行。下一节换一个维度——不是「素材能不能发」,而是「这个地区本身能不能进」:把制裁清单与地区限制变成一份可维护的地区表与一组工程开关。
阅读导航:上一节:11.3 税务表单与合规 · 下一节:12.2 制裁与地区可用性 。
继续阅读
探索更多技术文章
浏览归档,发现更多关于系统设计、工具链和工程实践的内容。