我的Flash收藏站


Flash收藏站

https://flash.hcyhub.com/
这是我的Flash收藏站,里面收藏了我喜欢的Flash游戏们。

我为什么要做这个网站?原因是2024年11月24日晚上,那天我抓取了“驾驶TU-95”这个Flash游戏,我小时候因为看不懂英文,一直坠机,那天晚上,我第一次尝试反编译SWF文件,成功汉化了第一个Flash游戏。

隔天,在昨天胜利的鼓舞下,我又汉化了第二个Flash游戏,也就在那时,我萌生了做一个自己的个人Flash游戏收藏站的想法,发布我自己汉化的游戏,在肝完汉化后随手写两个HTML文件就上传上去了,网站在2024.11.26成功发布。

刚刚发布时的样子

可以看到刚刚发布时极为简陋,页面就随手框了几个div容器,颜色都直接使用默认补全的颜色没调,所以就有了第一次修缮:

第一次页面修缮

2025-03-22 20:20:55

首页图片:

页脚:

在25年3月份,我装修了下页面,修改了颜色和页脚,新颜色看着舒服多了,这个主题配色也一直沿用至今成为了我最喜欢的风格,在当天,我还为了方便写了俩RSS生成器和Sitemap生成器。(当时还在使用手动创建所有站点文件,后来才搞了SSG一条龙)

当时写的RSS订阅文件生成器:

此RSS订阅生成器是通过读取我预留的Json元信息生成的,新链接的时间为第一次运行时间,链接的时间会储存在日志中,xml的优先级大于日志,可以手动修改xml RSS订阅时间,手动修改xml中的时间会在下一次程序运行时被更新进日志。

Json格式为:

[
    {
      "name": "驾驶TU-95",
      "desc": "驾驶战略轰炸机TU-95!带有汉化"
    },
    {
      "name": "文具防御",
      "desc": "使用文具打击来犯之敌!带有汉化"
    }
]
RSS生成器代码
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include <set>
#include <vector>
#include <ctime>
#include <json.hpp>

using json = nlohmann::json;

// RFC-822 格式时间
std::string getCurrentTime() {
    std::time_t now = std::time(nullptr);
    char buf[80];
    std::strftime(buf, sizeof(buf), "%a, %d %b %Y %H:%M:%S GMT", std::gmtime(&now));
    return std::string(buf);
}

std::string extractBetween(const std::string& str, const std::string& start_delim, const std::string& end_delim) {
    size_t start = str.find(start_delim);
    if (start == std::string::npos) return "";
    start += start_delim.size();
    size_t end = str.find(end_delim, start);
    if (end == std::string::npos) return "";
    return str.substr(start, end - start);
}

json readXmlPubDates(const std::string& xmlFilename) {
    json xmlMap = json::object();
    std::ifstream xmlFile(xmlFilename);
    if (!xmlFile.is_open())
        return xmlMap;

    std::string line;
    std::string currentLink;
    while (std::getline(xmlFile, line)) {
        if (line.find("<link>") != std::string::npos) {
            currentLink = extractBetween(line, "<link>", "</link>");
        } else if (line.find("<pubDate>") != std::string::npos) {
            std::string pubDate = extractBetween(line, "<pubDate>", "</pubDate>");
            if (!currentLink.empty()) {
                xmlMap[currentLink] = pubDate;
                currentLink.clear();
            }
        }
    }
    xmlFile.close();
    return xmlMap;
}

// 加载日志文件(日志是记录链接RSS时间的)
json loadLog(const std::string& logFilename) {
    json logData;
    std::ifstream logFile(logFilename);
    if (logFile.is_open()){
        try {
            logFile >> logData;
        } catch (json::parse_error& e) {
            std::cerr << "日志文件解析错误: " << e.what() << std::endl;
        }
        logFile.close();
    } else {
        logData = json::object();
    }
    return logData;
}

// 保存日志
void saveLog(const json& logData, const std::string& logFilename) {
    std::ofstream logFile(logFilename);
    if (logFile.is_open()){
        logFile << logData.dump(4);
        logFile.close();
    } else {
        std::cerr << "无法保存日志文件: " << logFilename << std::endl;
    }
}

#ifdef _WIN32
    #include <windows.h>
    #define timegm _mkgmtime
#endif

int main(int argc, char* argv[]) {
    std::string jsonInputFilename = "api/games_name.json";
    std::string xmlOutputFilename = "rss.xml";
    std::string logFilename = "rss_log.json";

    // --- 1. 读取 JSON  ---
    std::ifstream inputFile(jsonInputFilename);
    if (!inputFile.is_open()){
        std::cerr << "无法打开文件 " << jsonInputFilename << std::endl;
        return 1;
    }
    json jsonData;
    inputFile >> jsonData;
    inputFile.close();
    
    // --- 2. 加载日志 ---
    json logData = loadLog(logFilename);
    
    // --- 3. 尝试读取当前目录下的 rss.xml 中记录的时间(若已运行过) ---
    json xmlMap = readXmlPubDates(xmlOutputFilename);
    
    // 存储JSON链接
    std::set<std::string> jsonLinks;
    
    // --- 4. 生成 RSS XML 内容 ---
    std::ostringstream xmlContent;
    xmlContent << "<?xml version=\"1.0\" encoding=\"UTF-8\" ?>\n";
    xmlContent << "<rss version=\"2.0\">\n";
    xmlContent << "  <channel>\n";
    xmlContent << "    <title>Flash收藏站</title>\n";
    xmlContent << "    <link>https://flash.100713.xyz/</link>\n";
    xmlContent << "    <description>最新Flash游戏列表 RSS 订阅</description>\n";
    xmlContent << "    <language>zh-CN</language>\n";
    xmlContent << "    <lastBuildDate>" << getCurrentTime() << "</lastBuildDate>\n";
    
    for (const auto& item : jsonData) {
        std::string name = item["name"];
        std::string desc = item["desc"];
        std::string link = "https://flash.100713.xyz/" + name;
        jsonLinks.insert(link);
    
        std::string pubDate;
        if (item.contains("pubDate")) {
            pubDate = item["pubDate"];
        } else if (xmlMap.contains(link)) {
            pubDate = xmlMap[link];
        } else if (logData.contains(link)) {
            pubDate = logData[link];
        } else {
            pubDate = getCurrentTime();
        }
    
        if (!logData.contains(link) || logData[link] != pubDate) {
            logData[link] = pubDate;
        }
    
        xmlContent << "    <item>\n";
        xmlContent << "      <title>" << name << "</title>\n";
        xmlContent << "      <link>" << link << "</link>\n";
        xmlContent << "      <description>" << desc << "</description>\n";
        xmlContent << "      <pubDate>" << pubDate << "</pubDate>\n";
        xmlContent << "    </item>\n";
    }
    
    std::vector<std::string> keysToRemove;
    for (auto it = logData.begin(); it != logData.end(); ++it) {
        const std::string& link = it.key();
        if (jsonLinks.find(link) == jsonLinks.end()) {
            keysToRemove.push_back(link);
        }
    }
    for (const auto& link : keysToRemove) {
        logData.erase(link);
    }
    
    xmlContent << "  </channel>\n";
    xmlContent << "</rss>\n";
    
    std::ofstream outputFile(xmlOutputFilename);
    if (!outputFile.is_open()){
        std::cerr << "无法创建文件 " << xmlOutputFilename << std::endl;
        return 1;
    }
    outputFile << xmlContent.str();
    outputFile.close();
    
    // --- 更新的日志 ---
    saveLog(logData, logFilename);
    
    if (argc > 1 && std::strcmp(argv[1], "-c") == 0) {
        std::cout << "RSS 订阅链接文件生成完毕" << xmlOutputFilename << std::endl;
    } else {
#ifdef _WIN32
        HWND consoleWnd = GetConsoleWindow();
        if (consoleWnd != NULL) {
            ShowWindow(consoleWnd, SW_HIDE);
        }
        MessageBoxW(NULL, L"RSS 订阅链接文件生成完毕", L"OK!", MB_OK);
#else
        std::cout << "RSS 订阅链接文件生成完毕" << xmlOutputFilename << std::endl;
#endif
    }

    return 0;
}

站点地图生成器:

此站点地图生成器是通过读取RSS订阅文件来获取链接、链接标题、时间的

Sitemap生成器代码
#include <iostream>
#include <fstream>
#include <sstream>
#include <string>
#include "json.hpp"
#include <ctime>
#include <iomanip>
#include <cstring>

#ifdef _WIN32
    #include <windows.h>
    #define timegm _mkgmtime
#endif

using json = nlohmann::json;

std::string convertToSitemapTime(const std::string &rssTime) {
    std::tm tm = {};
    std::istringstream iss(rssTime);
    iss >> std::get_time(&tm, "%a, %d %b %Y %H:%M:%S GMT");
    if (iss.fail()) {
        std::cerr << "时间格式解析失败: " << rssTime << std::endl;
        return "";
    }
    time_t t = timegm(&tm);
    std::tm* gmt = std::gmtime(&t);
    
    char buf[80];
    std::strftime(buf, sizeof(buf), "%Y-%m-%dT%H:%M:%S+00:00", gmt);
    return std::string(buf);
}

std::string extractTitleFromUrl(const std::string &url) {
    size_t pos = url.find_last_of('/');
    if (pos == std::string::npos || pos + 1 >= url.size())
        return url;
    return url.substr(pos + 1);
}

int main(int argc, char* argv[]) {
    std::setlocale(LC_ALL, "");
    
    // 读取日志文件前面RSS订阅文件生成器的日志
    std::string logFilename = "rss_log.json";
    std::ifstream logFile(logFilename);
    if (!logFile.is_open()) {
        std::cerr << "无法打开日志文件: " << logFilename << std::endl;
        return 1;
    }
    
    json logData;
    try {
        logFile >> logData;
    } catch (json::parse_error &e) {
        std::cerr << "日志文件解析错误: " << e.what() << std::endl;
        return 1;
    }
    logFile.close();
    
    // 生成 sitemap.xml
    std::ofstream sitemapFile("sitemap.xml");
    if (!sitemapFile.is_open()) {
        std::cerr << "无法创建 sitemap.xml 文件" << std::endl;
        return 1;
    }
    
    sitemapFile << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n";
    sitemapFile << "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n";
    
    for (auto it = logData.begin(); it != logData.end(); ++it) {
        std::string url = it.key();
        std::string rssTime = it.value(); 
        std::string sitemapTime = convertToSitemapTime(rssTime);
        std::string title = extractTitleFromUrl(url);
        
        sitemapFile << "  <url>\n";
        sitemapFile << "    <loc>" << url << "</loc>\n";
        sitemapFile << "    <lastmod>" << sitemapTime << "</lastmod>\n";
        sitemapFile << "    <title>" << title << "</title>\n";
        sitemapFile << "  </url>\n";
    }
    sitemapFile << "</urlset>\n";
    sitemapFile.close();
    
    if (argc > 1 && std::strcmp(argv[1], "-c") == 0) {
        std::cout << "站点地图生成完毕" << std::endl;
    } else {
#ifdef _WIN32
        HWND consoleWnd = GetConsoleWindow();
        if (consoleWnd != NULL) {
            ShowWindow(consoleWnd, SW_HIDE);
        }
        
        MessageBoxW(NULL, L"站点地图生成完毕", L"OK!", MB_OK);
#else
        std::cout << "站点地图生成完毕" << std::endl;
#endif
    }
    
    return 0;
}

收藏站使用SSG生成器生成

2025-10-18 19:29:19

原站点目录

这,是原来Flash收藏站的站点根目录,可以看到核心部分是:

  • SWF ——SWF游戏文件
  • Games ——游戏页面HTML文件
  • api ——CSS、JS、图标文件、动态返回游戏列表
  • FlashWebPub——自己做的快速发布器,新架构弃用
  • / ——根目录下的首页站点地图RSS生成器等等

看着分类好像还挺不错的,找什么上哪找都很清晰对吧?是这样没错,但这135个文件全部是我人工一个个创建好的,当时发布游戏可不是说上传下文件就行,游戏页面是要我亲自复制模板大纲然后手动填充的(高级CV工程师名副其实),每发布一个游戏就得复制一份新的,要是以后想要修改页面样式还得一个个文件去编辑……想想就美好。

所以得赶在我发布了一大堆游戏前想出一个方法来,最好的办法莫过于:电脑代劳

电脑被发明出来就是为了处理信息的,很显然能让电脑干的活绝对不应该让人做,所以套模板填充信息这种脏话自然是得丢电脑头上。

现站点架构

这是现在的站点目录,相比之下根目录简洁多了,我花了一个下午的时间做了个静态站点生成器(SSG),现在发布文章就不需要人工新建页面了,只需要填写相关信息,一切脏活有生成器代劳,现在的目录结构是:

  • layout——模板文件
  • public——构建输出目录
  • source——数据来源目录
    • api——CSS、JS、列表文件
    • Game-data——游戏数据JSON存放目录
    • images——游戏图片
    • SWF——游戏文件

一目了然,我只需要上传游戏文件、图片,填写游戏数据,剩下的事均由生成器来干,站点地图?RSS?所有游戏的元信息?统统由它生成,我只管上传完文件后敲回车就行,并且首页图片也不再是由客户端获取列表生成,而是提前生成好客户端直接获取。

舒服多了现在,下次更新我要更新下页面,现在的播放页面效果有点差。(我好像好久没发布新的游戏了)

Flash收藏站

全新页面上线

2026-03-21 13:53:15

本次修改前的旧版页面

首页:

首页

游玩页面:

游玩页面

原来的页面怎么说呢,实在是一言难尽,用户体验极差,随便一看问题一大堆,比如首页卡片最后莫名其妙有俩空,游玩页面左右间距不一致下方按钮栏丑的要死,再仔细观察,播放窗口与边栏贴在一起,高度也不一致(边栏下面顶到了按钮栏)。总之就是站着看坐着看躺着看都很难受 闭着眼睛也难受。

于是在25年12月我开始慢慢修改页面,将其变得更美观使用更舒服。

新版页面

新版首页:

新版首页

新版播放页:

新版播放页

搜索功能(新增):

搜索功能

下载页面(新增):

下载页面

关于(新增):

关于

友链(新增):

友链

开发者列表(新增):

开发者列表

开发者详情页(新增):

开发者详情页

新版页面改动的地方非常多,足足五大项(小项修改不列举了,懒得翻日志):

  1. 加入统一导航栏、页脚
  2. 首页卡片展示区不再会有空了,20个一页(展示区还稍微内缩了一点)
  3. 删掉了播放页的按钮栏,加入显示作者、汉化者、发表日期等信息(目前暂放在边栏,没想好放哪好)
    1. 原先的下载按钮被合并至边栏下载按钮,在一个页面里显示、下载所有版本。
    2. 返回首页按钮功能由导航栏实现,导航栏明显用着更顺手更符合逻辑。
    3. 复制链接按钮删除(我要它何用。。。)
  4. 新增搜索功能,在大量游戏中搜索,避免费劲巴拉翻页半天
  5. 新增关于、友链、开发者汉化者页,丰富内容同时可以快速找到相同作者、汉化者的作品。

不仅页面外观看着更加舒适,播放窗口页面也改了,原先使用的是embed插入SWF文件,但是播放会出现渲染异常,例如素材外溢与显示错位,新版直接使用Ruffle创建播放器实例到窗口里,播放效果更佳。(由于直接使用Ruffle插入播放窗口了,下次更新还能加个放大缩小页面、全屏的功能。希望不要被我鸽太久)

——2026 年 2 月 13 日

2026.2.25更新

更新了底部翻页按钮,更加令人舒适

新版:

新版翻页按钮

旧版:

旧版翻页按钮

页首、侧边栏加上了图标

新版页首 侧边栏

此次更新加入了些许微交互,给予悬停反馈避免大马猴不会按按钮。关于页介绍页顺便更新了一下。

关于页介绍更新

其他的继续咕。

2026.2.28更新

底部介绍

更新底部介绍,把游戏相关信息从边栏移至此,免得边栏内容过多。

并顺便更新了下滚动条

2026.3.21更新

将以前创建的多篇收藏站文章合并到一起,也就是本篇。

说明区域修改 说明区域修改(移动端)

今天更新了说明区域,丰富了一下页面内容,在播放页使用首页卡片的图片(优化SEO)

并且我了解到了个Schema.org(JSON-LD)结构化数据,给搜索引擎看的实现富摘要的唯一途径,对于Flash收藏站而言,由于我很反感AI写的、“小编体”的介绍文章,导致我的页面上文章内容都很少,语义贫乏,SEO极低,加入了Schema.org后或许能有所改善?

2026.3.27更新

又是一次大更新,今天终于是填上了2026 年 2 月 13 日挖的坑——全屏按钮

今天更新的内容有三个,分别是:

  1. 屏幕键盘
  2. 全屏模式
  3. 多了个显示文件大小的文本内容

之所以添加屏幕键盘是因为大部分游戏都是需要键盘输入控制的,触摸屏设备在此之前大部分游戏无法游玩,今天我添加屏幕键盘后触摸屏就可以点击打开愉快玩耍了。

屏幕键盘效果:

屏幕键盘(桌面端) 屏幕键盘(移动端)

全屏模式效果:

全屏畅玩(桌面端) 全屏畅玩(移动端)

终于是把老早之前的坑给填上了,横屏全屏玩耍更舒服。

说明区域新增文件大小显示

说明区域给加上了个文件大小信息,没啥大区别纯凑凑字数丰富页面内容。

2026.3.28更新

网站上线了487天,今天终于是解决了一开始就存在困扰我很久的漏素材问题。

之前漏素材长这样:

之前漏素材

更新后长这样:

现在正常显示Flash播放范围

可以看到之前Ruffle的逻辑是尽量填满容器,所以它会把所有空的空间都渲染上,造成把本该在可视范围外隐藏的垃圾素材显示到播放窗口里,像上面这个最终导弹防御战还好整体挺干净的,像下面的这个毁灭之塔:

毁灭之塔

这乱七八糟的页面简直想让我把屏幕吃了,这个问题一直困扰着我,我咋设置showAll、letterbox都没用,直到今天彻底忍不了了,控制台输了一下console.table(window.RufflePlayer.config);,然后我看到:

(索引) 值
showSwfDownload true
ignoreOptout true
autoplay ‘auto’
unmuteOverlay ‘visible’
splashScreen true
openInNewTab ƒ

合计着变量压根没有应用?再一检查初始化代码:

document.addEventListener('DOMContentLoaded', function() {
    window.RufflePlayer = window.RufflePlayer || {};
    rufflePlayer = window.RufflePlayer.newest();
    player = rufflePlayer.createPlayer({
        allowScriptAccess: true,
        scale: "showall",
        forceScale: true,
        autoplay: "on",
        backgroundColor: null,
        polyfills: true,
        allowNetworking: "internal",
    });

发现有个问题,配置项赋值晚了,因为

rufflePlayer = window.RufflePlayer.newest();  // 读取 config(当时未配置是undefined)
window.RufflePlayer.config = {...};           // 这里我才赋值

Ruffle 的scale / letterbox属于 runtime 启动参数,只会在RufflePlayer.newest()调用时从window.RufflePlayer.config读取一次,于是Ruffle就按默认配置运行,结果就是Ruffle使用默认配置默认播放逻辑播放,默认配置下Ruffle会显示所有的素材,包括那些本应该被裁剪的部分。

而我发现问题的根源后就简单了,之前是”先读取,再赋值“,现在改成”先赋值,再读取“:

let player;
let rufflePlayer;

window.RufflePlayer = window.RufflePlayer || {};
window.RufflePlayer.config = {
    "letterbox": "on",
    "scale": "showAll",
    "forceScale": true,
    "autoplay": "on",
    "unmuteOverlay": "hidden"
};

启动无报错,成功解决素材外溢的问题。

再次运行console.table(window.RufflePlayer.config);,可以看到配置成功应用了:

(索引) 值
showSwfDownload true
ignoreOptout true
autoplay ‘on’
unmuteOverlay ‘hidden’
splashScreen true
letterbox ‘on’
scale ‘showAll’
forceScale true
openInNewTab ƒ
毁灭之塔(修复后)

强迫症极度舒适😇😇😇

侧边栏按钮修改

修改了下按钮样式,更加好看,原先默认按钮样式太丑了

2026.4.04更新

最后更新时间显示

没错,现在会游玩页会显示最近更新时间了。史诗级大更新,之所以突然给加了个这功能是因为今天汉化好了毁灭之塔,而毁灭之塔很早以前就发布了,静悄悄的更新页面估计没多少人发现所以加了个最后更新的时间显示,并且滚动到首页卡片流前面。

今天是2026年4.04日,推广一下我的404页面吧🤪

https://HcyHub.com/404

https://flash.HcyHub.com/404

2026.6.03~2026.6.12更新

史诗级更新,新增了分类、标签页,更好管理每一个Flash游戏、动画

标签页 分类页

同时新增了首页留言板、信息容器:

首页-2026.06.12

不得不说我设计的药丸按钮是真的好看。

同时进一步丰富了关于页,加上了FAQ和赞助码:

关于页-2026.06.12

另外我决定以后以黄行山这个代号做汉化作品了,因为汉化的受众都是中文用户,英文名Ethaniel不大好记、留下印象。

2026.10.11更新

更新了下首页留言本样式,可以显示根评论与子评论的层级嵌套、博主标签了。并且把丑不拉几的翻页按钮改成了滚动到最下面按加载更多按钮加载。

未来打算自己自定义一个Twikoo的评论区,目前用的是Twikoo官方提供的,感觉不大满意但还凑合。

2026.10.11首页留言本更新

其他

我之所以做了这个Flash收藏站,是因为我当时摸索着学会并汉化了第一个Flash游戏驾驶TU-95,汉化完成后却只能自己保存、无人知晓,这种感觉实在难受。所以我就糊了个首页跟播放页匆匆上线了Flash收藏站,用于展示我喜欢的游戏、我的汉化作品、别人的作品,收藏游戏作品供大家游玩。

这次页面大更新从25年12月左右开始花了差不多三个晚上,有灵感了这修修那改改,改完后上传看着那页面是越看越满意,虽然还是缺少了很多(比如游戏标签、游戏分类),但至少整体视觉一致性明显提升,看着舒服多了。

写到这里已经是 2026 年 2 月 13 日,页面大更新后一直想写个博客记录下结果拖到现在,马上过年了,马年快乐!

博客也好久没写了,忙着整音乐空间收藏站等其他子站。未来可能会把博客给移到blog子域下,HcyHub.com做个人空间。



文章作者: Ethaniel
版权声明: 本博客所有文章除特別声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来源 Ethaniel !
赏
  目录