1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
// ============================================================
// Steam Avatar Proxy - CS2D (Standalone Node.js, PNG with black fix)
// ============================================================
const fs = require('fs');
const path = require('path');
const axios = require('axios');
const sharp = require('sharp');
const PENDING_FILE = 'sys/pending_avatars.txt';
const AVATAR_DIR = 'mods/avatars/';
const MAX_RETRIES = 3;
const SCAN_INTERVAL_MS = 1000;
const REQUEST_TIMEOUT = 15000; // ms
// Create directories if they don't exist
if (!fs.existsSync('sys')) fs.mkdirSync('sys', { recursive: true });
if (!fs.existsSync(AVATAR_DIR)) fs.mkdirSync(AVATAR_DIR, { recursive: true });
if (!fs.existsSync(PENDING_FILE)) fs.writeFileSync(PENDING_FILE, '');
const retryMap = {};
let processing = false;
function avatarFile(steamid) {
// Now saving as PNG (required for color correction)
return path.join(AVATAR_DIR, `${steamid}.png`);
}
/**
* Downloads a Steam avatar, fixes absolute black pixels, saves as PNG.
* Returns true on success.
*/
async function downloadAvatar(steamid) {
const file = avatarFile(steamid);
if (fs.existsSync(file)) return true; // already cached
try {
// 1. Fetch Steam profile XML
const xmlUrl = `https://steamcommunity.com/profiles/${steamid}/?xml=1`;
const xmlRes = await axios.get(xmlUrl, {
timeout: REQUEST_TIMEOUT,
headers: { 'User-Agent': 'CS2D-Avatar-Proxy/2.0' }
});
if (xmlRes.status !== 200 || !xmlRes.data) {
throw new Error(`XML HTTP ${xmlRes.status}`);
}
// 2. Extract avatarMedium URL (64x64)
const regex = /<avatarMedium><!\[CDATA\[(https?:\/\/[^\]]+)\]\]><\/avatarMedium>/;
const match = xmlRes.data.match(regex);
if (!match) {
throw new Error('Avatar URL not found in XML');
}
let avatarUrl = match[1];
// Force HTTPS
avatarUrl = avatarUrl.replace(/^http:\/\//, 'https://');
// 3. Download the image (usually JPEG)
const imgRes = await axios.get(avatarUrl, {
timeout: REQUEST_TIMEOUT,
responseType: 'arraybuffer',
headers: { 'User-Agent': 'CS2D-Avatar-Proxy/2.0' }
});
if (imgRes.status !== 200 || !imgRes.data) {
throw new Error(`Image HTTP ${imgRes.status}`);
}
// 4. Convert to raw RGBA pixels and fix absolute black (#000000 -> #010101)
const raw = await sharp(imgRes.data)
.ensureAlpha()
.raw()
.toBuffer({ resolveWithObject: true });
// raw.data is a Buffer of RGBA pixels (each pixel = 4 bytes)
for (let i = 0; i < raw.data.length; i += 4) {
// Check for absolute black (R=0, G=0, B=0) and fully opaque (A=255)
if (raw.data[i] === 0 && raw.data[i+1] === 0 && raw.data[i+2] === 0 && raw.data[i+3] === 255) {
raw.data[i] = 1; // R -> 1
raw.data[i+1] = 1; // G -> 1
raw.data[i+2] = 1; // B -> 1
// A remains 255
}
}
// 5. Re-encode as PNG (lossless, preserves our pixel fix)
const finalPng = await sharp(raw.data, {
raw: {
width: raw.info.width,
height: raw.info.height,
channels: 4
}
}).png().toBuffer();
// 6. Save to disk
fs.writeFileSync(file, finalPng);
console.log(`[Proxy] Avatar saved (PNG, black-fixed): ${steamid}`);
return true;
} catch (error) {
console.log(`[Proxy] Error ${steamid}: ${error.message}`);
return false;
}
}
async function processPending() {
if (processing) return;
processing = true;
try {
const content = fs.readFileSync(PENDING_FILE, 'utf8');
let ids = content.split(/\r?\n/).map(v => v.trim()).filter(Boolean);
// Remove duplicates
ids = [...new Set(ids)];
if (ids.length === 0) {
processing = false;
return;
}
console.log(`[Proxy] Processing ${ids.length} pending...`);
const remaining = [];
for (const steamid of ids) {
if (retryMap[steamid] && retryMap[steamid] >= MAX_RETRIES) {
console.log(`[Proxy] Giving up on ${steamid} (${MAX_RETRIES} attempts)`);
continue;
}
const success = await downloadAvatar(steamid);
if (success) {
delete retryMap[steamid];
} else {
retryMap[steamid] = (retryMap[steamid] || 0) + 1;
if (retryMap[steamid] < MAX_RETRIES) {
remaining.push(steamid);
} else {
console.log(`[Proxy] Giving up on ${steamid} (${MAX_RETRIES} attempts)`);
}
}
}
fs.writeFileSync(PENDING_FILE, remaining.join('\n') + (remaining.length > 0 ? '\n' : ''));
console.log(`[Proxy] Success: ${ids.length - remaining.length} OK, ${remaining.length} pending`);
} catch (error) {
console.error('[Proxy] Processing error:', error);
} finally {
processing = false;
}
}
console.log('[Proxy] Started (standalone mode, PNG + black fix)');
setTimeout(processPending, 1000);
setInterval(processPending, SCAN_INTERVAL_MS);
process.on('SIGINT', () => { console.log('[Proxy] Shutting down'); process.exit(0); });
process.on('unhandledRejection', (err) => { console.error('[Proxy] Unhandled rejection:', err); });