playworks

This commit is contained in:
2026-05-28 11:47:26 +08:00
parent 274bafe1cb
commit 4f17523f26
4 changed files with 169 additions and 36 deletions

Binary file not shown.

Binary file not shown.

View File

@@ -95,10 +95,11 @@ export function openPlayworksConverter(_context: vscode.ExtensionContext) {
} }
const ZIPPED_NETWORKS = new Set(['gg', 'vu', 'mtg']); const ZIPPED_NETWORKS = new Set(['gg', 'vu', 'mtg']);
const SUPPORTED_NETWORKS = new Set(['al', 'is', 'fb', 'gg', 'mo', 'vu', 'mtg', 'tt']);
const MRAID_NETWORKS = new Set(['al', 'is']);
const NETWORK_OUTPUT_FOLDERS: Record<string, string> = { const NETWORK_OUTPUT_FOLDERS: Record<string, string> = {
al: 'Applovin', al: 'Applovin',
is: 'Ironsource', is: 'Ironsource',
un: 'Unity',
fb: 'Facebook', fb: 'Facebook',
gg: 'GoogleAds', gg: 'GoogleAds',
mo: 'Moloco', mo: 'Moloco',
@@ -120,6 +121,9 @@ function outputBaseName(opts: ConvertOptions): string {
} }
async function convertNetwork(html: string, opts: ConvertOptions, network: string): Promise<string> { async function convertNetwork(html: string, opts: ConvertOptions, network: string): Promise<string> {
if (!SUPPORTED_NETWORKS.has(network)) {
throw new Error(`Unsupported Playworks target network: ${network}`);
}
const transformed = transformHtml(html, network); const transformed = transformHtml(html, network);
const needsZip = ZIPPED_NETWORKS.has(network); const needsZip = ZIPPED_NETWORKS.has(network);
const baseName = outputBaseName(opts); const baseName = outputBaseName(opts);
@@ -150,34 +154,68 @@ function transformHtml(html: string, network: string): string {
// Inject into </head>: lifecycle stubs only (must register luna:build listener // Inject into </head>: lifecycle stubs only (must register luna:build listener
// BEFORE the Playworks bundle so our handler fires first), then any network SDK script. // BEFORE the Playworks bundle so our handler fires first), then any network SDK script.
// Console restore stays at end-of-body — it must run AFTER the bundle overrides console. // Console restore stays at end-of-body — it must run AFTER the bundle overrides console.
result = removeNetworkSdkScripts(result, network);
let headInject = buildLifecycleScript(); let headInject = buildLifecycleScript();
if (network === 'al' || network === 'is') { if (MRAID_NETWORKS.has(network)) {
headInject += '<script src="mraid.js"></script>'; headInject += '<script src="mraid.js"></script>';
} else if (network === 'gg') { } else if (network === 'gg') {
headInject += '<script src="exitapi.js"></script>'; headInject += '<script src="exitapi.js"></script>';
} }
result = result.replace('</head>', headInject + '\n</head>'); result = injectBeforeHeadClose(result, headInject);
// Body-level flags // Body-level flags
if (network === 'vu') { if (network === 'vu') {
result = result.replace('<body>', '<body>\n<script>window.__VUNGLE__=true;</script>'); result = injectAfterBodyOpen(result, '<script>window.__VUNGLE__=true;</script>');
} else if (network === 'mtg') { } else if (network === 'mtg') {
result = result.replace('<body>', '<body onload="gameReady()">'); result = addBodyOnload(result, 'gameReady()');
} else if (network === 'tt') { } else if (network === 'tt') {
result = result.replace('<body>', '<body>\n<script>window.__TIKTOK__=true;</script>'); result = injectAfterBodyOpen(result, '<script>window.__TIKTOK__=true;</script>');
} }
// Replace the CTA script // Replace the CTA script
result = replaceCTAScript(result, network); result = replaceCTAScript(result, network);
// Unity: rewrite window.top → window.self (Luna static scan requirement)
if (network === 'un') {
result = result.split('window.top').join('window.self');
}
return result; return result;
} }
function removeNetworkSdkScripts(html: string, network: string): string {
let result = html;
if (MRAID_NETWORKS.has(network)) {
result = result.replace(/<script\b[^>]*\bsrc\s*=\s*["']mraid\.js["'][^>]*>\s*<\/script>\s*/gi, '');
}
if (network === 'gg') {
result = result.replace(/<script\b[^>]*\bsrc\s*=\s*["']exitapi\.js["'][^>]*>\s*<\/script>\s*/gi, '');
}
return result;
}
function injectBeforeHeadClose(html: string, injection: string): string {
if (/<\/head>/i.test(html)) {
return html.replace(/<\/head>/i, `${injection}\n</head>`);
}
return `${injection}\n${html}`;
}
function injectAfterBodyOpen(html: string, injection: string): string {
if (/<body\b[^>]*>/i.test(html)) {
return html.replace(/<body\b[^>]*>/i, match => `${match}\n${injection}`);
}
return `${injection}\n${html}`;
}
function addBodyOnload(html: string, handler: string): string {
if (!/<body\b[^>]*>/i.test(html)) {
return `<body onload="${handler}">\n${html}`;
}
return html.replace(/<body\b([^>]*)>/i, (match, attrs: string) => {
const onloadMatch = attrs.match(/\bonload\s*=\s*(["'])(.*?)\1/i);
if (!onloadMatch) return `<body${attrs} onload="${handler}">`;
if (onloadMatch[2].includes(handler)) return match;
const updated = `${onloadMatch[2]};${handler}`;
return match.replace(onloadMatch[0], `onload=${onloadMatch[1]}${updated}${onloadMatch[1]}`);
});
}
function replaceCTAScript(html: string, network: string): string { function replaceCTAScript(html: string, network: string): string {
const marker = 'Luna.Unity.Playable.InstallFullGame=function'; const marker = 'Luna.Unity.Playable.InstallFullGame=function';
const markerIdx = html.lastIndexOf(marker); const markerIdx = html.lastIndexOf(marker);
@@ -245,11 +283,7 @@ function buildCTAScript(network: string): string {
case 'al': case 'al':
case 'is': case 'is':
// MRAID — mraid.js injected in <head> // MRAID — mraid.js injected in <head>
ctaLogic = `${closeCall}if(typeof mraid!=="undefined"&&typeof mraid.open==="function"){if(typeof mraid.getState==="function"&&mraid.getState()==="loading"){mraid.addEventListener("ready",function(){mraid.open(o)});}else{mraid.open(o);}}else{window.open(o,"_blank");}`; ctaLogic = `${closeCall}if(typeof mraid!=="undefined"&&typeof mraid.open==="function"){var s=typeof mraid.getState==="function"?mraid.getState():"default";if(s!=="loading"){mraid.open(o);return;}}window.open(o,"_blank");`;
break;
case 'un':
// Unity Ads — MRAID-based; window.top already rewritten separately
ctaLogic = `${closeCall}if(typeof mraid!=="undefined"&&typeof mraid.open==="function"){mraid.open(o);}else{window.open(o,"_blank");}`;
break; break;
case 'fb': case 'fb':
ctaLogic = `${closeCall}if(typeof FbPlayableAd!=="undefined"&&typeof FbPlayableAd.onCTAClick==="function"){FbPlayableAd.onCTAClick();return;}window.open(o,"_blank");`; ctaLogic = `${closeCall}if(typeof FbPlayableAd!=="undefined"&&typeof FbPlayableAd.onCTAClick==="function"){FbPlayableAd.onCTAClick();return;}window.open(o,"_blank");`;
@@ -259,7 +293,7 @@ function buildCTAScript(network: string): string {
ctaLogic = `${closeCall}if(typeof ExitApi!=="undefined"&&typeof ExitApi.exit==="function"){ExitApi.exit();return;}window.open(o,"_blank");`; ctaLogic = `${closeCall}if(typeof ExitApi!=="undefined"&&typeof ExitApi.exit==="function"){ExitApi.exit();return;}window.open(o,"_blank");`;
break; break;
case 'mo': case 'mo':
ctaLogic = `${closeCall}if(typeof window.clickTag==="string"&&window.clickTag){window.open(window.clickTag,"_blank");return;}window.open(o,"_blank");`; ctaLogic = `${closeCall}if(typeof FbPlayableAd!=="undefined"&&typeof FbPlayableAd.onCTAClick==="function"){FbPlayableAd.onCTAClick();return;}if(typeof window.clickTag==="string"&&window.clickTag){window.open(window.clickTag,"_blank");return;}window.open(o,"_blank");`;
break; break;
case 'vu': case 'vu':
// Vungle — window.__VUNGLE__ flag set in body // Vungle — window.__VUNGLE__ flag set in body
@@ -314,10 +348,9 @@ function getHtml(): string {
const networks = [ const networks = [
{ tag: 'al', label: 'Applovin', note: 'HTML + mraid.js in head' }, { tag: 'al', label: 'Applovin', note: 'HTML + mraid.js in head' },
{ tag: 'is', label: 'Ironsource', note: 'HTML + mraid.js in head' }, { tag: 'is', label: 'Ironsource', note: 'HTML + mraid.js in head' },
{ tag: 'un', label: 'Unity', note: 'HTML (window.top → self)' },
{ tag: 'fb', label: 'Facebook', note: 'HTML' }, { tag: 'fb', label: 'Facebook', note: 'HTML' },
{ tag: 'gg', label: 'Google Ads', note: 'ZIP + exitapi.js in head' }, { tag: 'gg', label: 'Google Ads', note: 'ZIP + exitapi.js in head' },
{ tag: 'mo', label: 'Moloco', note: 'HTML' }, { tag: 'mo', label: 'Moloco', note: 'HTML + FbPlayableAd CTA' },
{ tag: 'vu', label: 'Vungle', note: 'ZIP + __VUNGLE__ flag' }, { tag: 'vu', label: 'Vungle', note: 'ZIP + __VUNGLE__ flag' },
{ tag: 'mtg', label: 'Mintegral', note: 'ZIP + onload="gameReady()"' }, { tag: 'mtg', label: 'Mintegral', note: 'ZIP + onload="gameReady()"' },
{ tag: 'tt', label: 'TikTok', note: 'HTML + __TIKTOK__ flag' }, { tag: 'tt', label: 'TikTok', note: 'HTML + __TIKTOK__ flag' },

View File

@@ -77,10 +77,9 @@ func PlayworksPage(w http.ResponseWriter, r *http.Request) {
<div class="net-grid"> <div class="net-grid">
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="al" checked /><span class="net-label">Applovin <span class="net-tag">al</span></span><span class="net-note">HTML + mraid.js in head</span></label> <label class="net-row"><input type="checkbox" class="net-cb" data-tag="al" checked /><span class="net-label">Applovin <span class="net-tag">al</span></span><span class="net-note">HTML + mraid.js in head</span></label>
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="is" checked /><span class="net-label">Ironsource <span class="net-tag">is</span></span><span class="net-note">HTML + mraid.js in head</span></label> <label class="net-row"><input type="checkbox" class="net-cb" data-tag="is" checked /><span class="net-label">Ironsource <span class="net-tag">is</span></span><span class="net-note">HTML + mraid.js in head</span></label>
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="un" checked /><span class="net-label">Unity <span class="net-tag">un</span></span><span class="net-note">HTML (window.top to self)</span></label>
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="fb" checked /><span class="net-label">Facebook <span class="net-tag">fb</span></span><span class="net-note">HTML</span></label> <label class="net-row"><input type="checkbox" class="net-cb" data-tag="fb" checked /><span class="net-label">Facebook <span class="net-tag">fb</span></span><span class="net-note">HTML</span></label>
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="gg" checked /><span class="net-label">Google Ads <span class="net-tag">gg</span></span><span class="net-note">ZIP + exitapi.js in head</span></label> <label class="net-row"><input type="checkbox" class="net-cb" data-tag="gg" checked /><span class="net-label">Google Ads <span class="net-tag">gg</span></span><span class="net-note">ZIP + exitapi.js in head</span></label>
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="mo" checked /><span class="net-label">Moloco <span class="net-tag">mo</span></span><span class="net-note">HTML</span></label> <label class="net-row"><input type="checkbox" class="net-cb" data-tag="mo" checked /><span class="net-label">Moloco <span class="net-tag">mo</span></span><span class="net-note">HTML + FbPlayableAd CTA</span></label>
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="vu" checked /><span class="net-label">Vungle <span class="net-tag">vu</span></span><span class="net-note">ZIP + __VUNGLE__ flag</span></label> <label class="net-row"><input type="checkbox" class="net-cb" data-tag="vu" checked /><span class="net-label">Vungle <span class="net-tag">vu</span></span><span class="net-note">ZIP + __VUNGLE__ flag</span></label>
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="mtg" checked /><span class="net-label">Mintegral <span class="net-tag">mtg</span></span><span class="net-note">ZIP + onload gameReady</span></label> <label class="net-row"><input type="checkbox" class="net-cb" data-tag="mtg" checked /><span class="net-label">Mintegral <span class="net-tag">mtg</span></span><span class="net-note">ZIP + onload gameReady</span></label>
<label class="net-row"><input type="checkbox" class="net-cb" data-tag="tt" /><span class="net-label">TikTok <span class="net-tag">tt</span></span><span class="net-note">HTML + __TIKTOK__ flag</span></label> <label class="net-row"><input type="checkbox" class="net-cb" data-tag="tt" /><span class="net-label">TikTok <span class="net-tag">tt</span></span><span class="net-note">HTML + __TIKTOK__ flag</span></label>
@@ -230,6 +229,11 @@ func PlayworksConvert(w http.ResponseWriter, r *http.Request) {
} }
var playworksSuffixRx = regexp.MustCompile(`_(?:un|al|is|fb|gg|mo|vu|mtg|tt)$`) var playworksSuffixRx = regexp.MustCompile(`_(?:un|al|is|fb|gg|mo|vu|mtg|tt)$`)
var playworksMraidScriptRx = regexp.MustCompile(`(?i)<script\b[^>]*\bsrc\s*=\s*["']mraid\.js["'][^>]*>\s*</script>\s*`)
var playworksExitapiScriptRx = regexp.MustCompile(`(?i)<script\b[^>]*\bsrc\s*=\s*["']exitapi\.js["'][^>]*>\s*</script>\s*`)
var playworksHeadCloseRx = regexp.MustCompile(`(?i)</head>`)
var playworksBodyOpenRx = regexp.MustCompile(`(?i)<body\b[^>]*>`)
var playworksBodyTagRx = regexp.MustCompile(`(?i)<body\b([^>]*)>`)
func playworksSourceBaseName(sourcePath string) string { func playworksSourceBaseName(sourcePath string) string {
base := strings.TrimSuffix(filepath.Base(sourcePath), filepath.Ext(sourcePath)) base := strings.TrimSuffix(filepath.Base(sourcePath), filepath.Ext(sourcePath))
@@ -259,6 +263,9 @@ func playworksOutputBaseName(opts playworksConvertOptions) string {
} }
func convertPlayworksNetwork(htmlSrc string, opts playworksConvertOptions, network string) (string, error) { func convertPlayworksNetwork(htmlSrc string, opts playworksConvertOptions, network string) (string, error) {
if !playworksIsSupportedNetwork(network) {
return "", fmt.Errorf("unsupported Playworks target network: %s", network)
}
transformed := transformPlayworksHTML(htmlSrc, network) transformed := transformPlayworksHTML(htmlSrc, network)
baseName := playworksOutputBaseName(opts) baseName := playworksOutputBaseName(opts)
htmlFileName := fmt.Sprintf("%s_%s.html", baseName, network) htmlFileName := fmt.Sprintf("%s_%s.html", baseName, network)
@@ -288,8 +295,6 @@ func playworksNetworkOutputFolder(network string) string {
return "Applovin" return "Applovin"
case "is": case "is":
return "Ironsource" return "Ironsource"
case "un":
return "Unity"
case "fb": case "fb":
return "Facebook" return "Facebook"
case "gg": case "gg":
@@ -311,33 +316,130 @@ func playworksNeedsZip(network string) bool {
return network == "gg" || network == "vu" || network == "mtg" return network == "gg" || network == "vu" || network == "mtg"
} }
func playworksIsMraidNetwork(network string) bool {
return network == "al" || network == "is"
}
func playworksIsSupportedNetwork(network string) bool {
switch network {
case "al", "is", "fb", "gg", "mo", "vu", "mtg", "tt":
return true
default:
return false
}
}
func transformPlayworksHTML(htmlSrc, network string) string { func transformPlayworksHTML(htmlSrc, network string) string {
result := htmlSrc result := htmlSrc
result = removePlayworksNetworkScripts(result, network)
headInject := buildPlayworksLifecycleScript() headInject := buildPlayworksLifecycleScript()
if network == "al" || network == "is" { if playworksIsMraidNetwork(network) {
headInject += `<script src="mraid.js"></script>` headInject += `<script src="mraid.js"></script>`
} else if network == "gg" { } else if network == "gg" {
headInject += `<script src="exitapi.js"></script>` headInject += `<script src="exitapi.js"></script>`
} }
result = strings.Replace(result, "</head>", headInject+"\n</head>", 1) result = injectPlayworksBeforeHeadClose(result, headInject)
if network == "vu" { if network == "vu" {
result = strings.Replace(result, "<body>", `<body> result = injectPlayworksAfterBodyOpen(result, `<script>window.__VUNGLE__=true;</script>`)
<script>window.__VUNGLE__=true;</script>`, 1)
} else if network == "mtg" { } else if network == "mtg" {
result = strings.Replace(result, "<body>", `<body onload="gameReady()">`, 1) result = addPlayworksBodyOnload(result, "gameReady()")
} else if network == "tt" { } else if network == "tt" {
result = strings.Replace(result, "<body>", `<body> result = injectPlayworksAfterBodyOpen(result, `<script>window.__TIKTOK__=true;</script>`)
<script>window.__TIKTOK__=true;</script>`, 1)
} }
result = replacePlayworksCTAScript(result, network) result = replacePlayworksCTAScript(result, network)
if network == "un" { return result
result = strings.ReplaceAll(result, "window.top", "window.self") }
func removePlayworksNetworkScripts(htmlSrc, network string) string {
result := htmlSrc
if playworksIsMraidNetwork(network) {
result = playworksMraidScriptRx.ReplaceAllString(result, "")
}
if network == "gg" {
result = playworksExitapiScriptRx.ReplaceAllString(result, "")
} }
return result return result
} }
func injectPlayworksBeforeHeadClose(htmlSrc, injection string) string {
if playworksHeadCloseRx.MatchString(htmlSrc) {
return playworksHeadCloseRx.ReplaceAllString(htmlSrc, injection+"\n</head>")
}
return injection + "\n" + htmlSrc
}
func injectPlayworksAfterBodyOpen(htmlSrc, injection string) string {
loc := playworksBodyOpenRx.FindStringIndex(htmlSrc)
if loc == nil {
return injection + "\n" + htmlSrc
}
return htmlSrc[:loc[1]] + "\n" + injection + htmlSrc[loc[1]:]
}
func addPlayworksBodyOnload(htmlSrc, handler string) string {
loc := playworksBodyTagRx.FindStringSubmatchIndex(htmlSrc)
if loc == nil {
return `<body onload="` + handler + `">` + "\n" + htmlSrc
}
attrs := htmlSrc[loc[2]:loc[3]]
onloadStart, onloadEnd, quote, valueStart, valueEnd := findPlayworksOnloadAttr(attrs)
if onloadStart == -1 {
updated := `<body` + attrs + ` onload="` + handler + `">`
return htmlSrc[:loc[0]] + updated + htmlSrc[loc[1]:]
}
value := attrs[valueStart:valueEnd]
if strings.Contains(value, handler) {
return htmlSrc
}
replacement := `onload=` + quote + value + `;` + handler + quote
updatedAttrs := attrs[:onloadStart] + replacement + attrs[onloadEnd:]
updatedTag := `<body` + updatedAttrs + `>`
return htmlSrc[:loc[0]] + updatedTag + htmlSrc[loc[1]:]
}
func findPlayworksOnloadAttr(attrs string) (start int, end int, quote string, valueStart int, valueEnd int) {
lower := strings.ToLower(attrs)
idx := strings.Index(lower, "onload")
for idx != -1 {
if idx > 0 {
prev := lower[idx-1]
if (prev >= 'a' && prev <= 'z') || (prev >= '0' && prev <= '9') || prev == '-' || prev == '_' {
next := strings.Index(lower[idx+len("onload"):], "onload")
if next == -1 {
return -1, -1, "", -1, -1
}
idx += len("onload") + next
continue
}
}
pos := idx + len("onload")
for pos < len(attrs) && (attrs[pos] == ' ' || attrs[pos] == '\t' || attrs[pos] == '\n' || attrs[pos] == '\r') {
pos++
}
if pos >= len(attrs) || attrs[pos] != '=' {
return -1, -1, "", -1, -1
}
pos++
for pos < len(attrs) && (attrs[pos] == ' ' || attrs[pos] == '\t' || attrs[pos] == '\n' || attrs[pos] == '\r') {
pos++
}
if pos >= len(attrs) || (attrs[pos] != '"' && attrs[pos] != '\'') {
return -1, -1, "", -1, -1
}
q := attrs[pos]
valueStart = pos + 1
valueEnd = strings.IndexByte(attrs[valueStart:], q)
if valueEnd == -1 {
return -1, -1, "", -1, -1
}
valueEnd += valueStart
return idx, valueEnd + 1, string(q), valueStart, valueEnd
}
return -1, -1, "", -1, -1
}
func replacePlayworksCTAScript(htmlSrc, network string) string { func replacePlayworksCTAScript(htmlSrc, network string) string {
marker := "Luna.Unity.Playable.InstallFullGame=function" marker := "Luna.Unity.Playable.InstallFullGame=function"
markerIdx := strings.LastIndex(htmlSrc, marker) markerIdx := strings.LastIndex(htmlSrc, marker)
@@ -393,15 +495,13 @@ func buildPlayworksCTAScript(network string) string {
ctaLogic := closeCall + `window.open(o,"_blank");` ctaLogic := closeCall + `window.open(o,"_blank");`
switch network { switch network {
case "al", "is": case "al", "is":
ctaLogic = closeCall + `if(typeof mraid!=="undefined"&&typeof mraid.open==="function"){if(typeof mraid.getState==="function"&&mraid.getState()==="loading"){mraid.addEventListener("ready",function(){mraid.open(o)});}else{mraid.open(o);}}else{window.open(o,"_blank");}` ctaLogic = closeCall + `if(typeof mraid!=="undefined"&&typeof mraid.open==="function"){var s=typeof mraid.getState==="function"?mraid.getState():"default";if(s!=="loading"){mraid.open(o);return;}}window.open(o,"_blank");`
case "un":
ctaLogic = closeCall + `if(typeof mraid!=="undefined"&&typeof mraid.open==="function"){mraid.open(o);}else{window.open(o,"_blank");}`
case "fb": case "fb":
ctaLogic = closeCall + `if(typeof FbPlayableAd!=="undefined"&&typeof FbPlayableAd.onCTAClick==="function"){FbPlayableAd.onCTAClick();return;}window.open(o,"_blank");` ctaLogic = closeCall + `if(typeof FbPlayableAd!=="undefined"&&typeof FbPlayableAd.onCTAClick==="function"){FbPlayableAd.onCTAClick();return;}window.open(o,"_blank");`
case "gg": case "gg":
ctaLogic = closeCall + `if(typeof ExitApi!=="undefined"&&typeof ExitApi.exit==="function"){ExitApi.exit();return;}window.open(o,"_blank");` ctaLogic = closeCall + `if(typeof ExitApi!=="undefined"&&typeof ExitApi.exit==="function"){ExitApi.exit();return;}window.open(o,"_blank");`
case "mo": case "mo":
ctaLogic = closeCall + `if(typeof window.clickTag==="string"&&window.clickTag){window.open(window.clickTag,"_blank");return;}window.open(o,"_blank");` ctaLogic = closeCall + `if(typeof FbPlayableAd!=="undefined"&&typeof FbPlayableAd.onCTAClick==="function"){FbPlayableAd.onCTAClick();return;}if(typeof window.clickTag==="string"&&window.clickTag){window.open(window.clickTag,"_blank");return;}window.open(o,"_blank");`
case "vu": case "vu":
ctaLogic = closeCall + `try{parent.postMessage("download","*");}catch(e){}` ctaLogic = closeCall + `try{parent.postMessage("download","*");}catch(e){}`
case "mtg": case "mtg":