Tool QSP RPGM Unity HTML Java Ren'Py Flash Wolf RPG UltraFast adult games compressor v3.1.3.12

5.00 star(s) 3 Votes

For what compressor u want mass/bulk game compress feature?

  • RPGM MV/MZ

    Votes: 8 22.2%
  • HTML

    Votes: 2 5.6%
  • I want be able to bulk compress all engines (this is not planned since too hard to add)

    Votes: 1 2.8%
  • I want to compress all xp3 files in game folder (no u don't coz some games are encrypted)

    Votes: 3 8.3%
  • I don't want this feature at all so don't waste ur time on it, just enjoy ur summer time :)

    Votes: 3 8.3%
  • I have potato PC

    Votes: 13 36.1%
  • I have average PC

    Votes: 8 22.2%
  • I have fast PC

    Votes: 8 22.2%
  • I have super PC

    Votes: 2 5.6%

  • Total voters
    36
  • Poll closed .

MrCrisis

Newbie
Jul 21, 2018
32
4
Hi, thanks for your work. Here a little feature request:

Feature request descriptionUAGC version, settings screenshotOS, CPU, RAM, HDD/SDD (opt)Link to the game (req for bugrep)Link to specific file (optional)

Improve the "Renpy Compression Assistant" for video.


I'm doing a lot of AV1 (av1_nvenc) encoding tests using variable bitrate.
Would it be possible to add an option to keep the original file if the encoded version ends up being larger?


Ideally, it should be possible to specify a minimum size difference in percent (e.g., 10%) required to keep the encoded version.
In my example, if the encoded file isn't at least 10% smaller, the original file should be kept instead.

UAGC 3.1.3 v7Windows 11
Ryzen 3950x
64GB

Windows 11
Dual E5-2667 V4
512 GB
1752682673735.png

On a separate note, I’d recommend updating to the latest version of NConvert (apologies if it has already been done).
The new version is much more user-friendly when it comes to handling AVIF parameters.


1752683109713.png
 

megalol

Engaged Member
Modder
Compressor
Respected User
Apr 3, 2017
2,809
11,732
Hi, thanks for your work. Here a little feature request:

Feature request descriptionUAGC version, settings screenshotOS, CPU, RAM, HDD/SDD (opt)Link to the game (req for bugrep)Link to specific file (optional)

Improve the "Renpy Compression Assistant" for video.


I'm doing a lot of AV1 (av1_nvenc) encoding tests using variable bitrate.
Would it be possible to add an option to keep the original file if the encoded version ends up being larger?


Ideally, it should be possible to specify a minimum size difference in percent (e.g., 10%) required to keep the encoded version.
In my example, if the encoded file isn't at least 10% smaller, the original file should be kept instead.
UAGC 3.1.3 v7Windows 11
Ryzen 3950x
64GB

Windows 11
Dual E5-2667 V4
512 GB
View attachment 5048211

On a separate note, I’d recommend updating to the latest version of NConvert (apologies if it has already been done).
The new version is much more user-friendly when it comes to handling AVIF parameters.


View attachment 5048229
I dunno why u mentioned RenPy cmpr assistant since it already implemented long time inside main RenPy compressor, only problem that it doesn't have av1_nvenc yet (only SVT-AV1) like dedicated RenPy avif/AV1 compressor but I have plans to add it in future (I meant av1_nvenc in RenPy main compressor). 1752684026145.png
About nconvert for avif count it as depricated that left just for fun and u should use imagemagick instead (just untick nconvert at ur image) for avif since it's "much more user-friendly when it comes to handling AVIF parameters".
 
Last edited:

MrCrisis

Newbie
Jul 21, 2018
32
4
Thanks for the quick response!


Great news! I'm really looking forward to seeing this feature added for the AVIF/AV1 section.
If the "minimum ratio" option could also be applied to videos, that would be ideal.
(If I understood the description correctly, it's currently only used for images.)

Alright, regarding ImageMagick, i have to admit I preferred using NConvert,
mainly because setting the quality with a simple -q [0-100] felt much easier than using
-avif_quant_color X X X and -avif_quant_alpha X X X.

But no worries, i’ll push myself and make the switch to ImageMagick :LOL:.


EDIT:


Oh sorry, I just noticed the edit in your message.
My bad, I wasn’t aware that AV1/AVIF encoding was already available on the main ren'Py page.
From what I understand, everything will be moved to the main Ren'Py compression page, and the dedicated page will no longer be needed?

If I got it right, the main Ren'Py page already allows encoding to AVIF and AV1, it’s just missing AV1_NVENC support?
Also, can the main page update the Ren'Py engine the same way the dedicated one does? (maybe this 1752687031771.png )
 
Last edited:

megalol

Engaged Member
Modder
Compressor
Respected User
Apr 3, 2017
2,809
11,732
If I got it right, the main Ren'Py page already allows encoding to AVIF and AV1, it’s just missing AV1_NVENC support?
Also, can the main page update the Ren'Py engine the same way the dedicated one does? (maybe this View attachment 5048462 )
Yes. Yes (u need also select in UAGC settings PC for SDK building type) 1752698507918.png to re(build) Win/Lin version instead of MacOS one and enable this too 1752698751579.png . But u need it only if current RenPy game engine version doesn't supports avif/AV1. Also in UAGC settings exists feature to auto compress to avif/AV1 if RenPy engine version supports it.
 
Last edited:
  • Like
Reactions: Discoplayer1

megalol

Engaged Member
Modder
Compressor
Respected User
Apr 3, 2017
2,809
11,732
I've told AI for fun to write script for grease/tampermonkey that adds direct compressed link to OP, it's not ideal for games that has multiple chapters/seasons for example or if compressor posted compressed version before OP was updated but better than nothing, if OP has compressed word in OP it wouldn't do anything (to avoid useless f95zone server load). Problem is that ppl that could install this script usually have no problems with finding latest compressed version.
JavaScript:
// ==UserScript==
// @name F95 – add “Compressed version” link (Fixed skip check + pagination + debug + tentative & technical filtering; skip Mods forum)
// @namespace https://example.com/
// @version 1.8.14
// @description Adds compressed version link above downloads with pagination; skips only if link already inserted, with debug logs included. Filters tentative announcements and technical posts to avoid false positives. Skips Mods forum threads. Adjusted to avoid treating plain "download" as a positive indicator and tuned request detection. (Patched: improved quote/request & technical filters + page-reference skip in search results)
// @author You
// @match https://f95zone.to/*threads/*
// @grant GM_xmlhttpRequest
// @connect f95zone.to
// ==/UserScript==

/* -------------------------------------------------- /
/ ------------------ helpers ----------------------- /
/ -------------------------------------------------- */

function parseVersionKey(text) {
    if (!text) return null;
    const cleanedText = text.replace(/[[]]/g, ' ');


    // Season + chapter/episode pattern (e.g., "S2 Ch. 3", "Season 1 Ep 12")
    const seasonRegex = /s(?:eason)?\s*(\d+).*?(ch|ep|chapter|episode)\.?\s*(\d+)/i;
    let match = cleanedText.match(seasonRegex);
    if (match && match[1] && match[2] && match[3]) {
        const season = parseInt(match[1], 10);
        const typeStr = match[2].toLowerCase();
        const number = parseInt(match[3], 10);
        const displayType = typeStr.startsWith('ch') ? 'Ch.' : 'Ep.';
        console.debug(`parseVersionKey: matched season/chapter: S${season} ${displayType} ${number}`);
        return { key: season + number / 100, valueStr: `S${season} ${displayType} ${number}` };
    }

    // Standalone chapter/episode (e.g., "Ch. 3", "Episode 5")
    const standaloneRegex = /\b(ch|ep|chapter|episode)\.?\s*(\d+)/i;
    match = cleanedText.match(standaloneRegex);
    if (match && match[1] && match[2]) {
        const typeStr = match[1].toLowerCase();
        const number = parseInt(match[2], 10);
        const displayType = typeStr.startsWith('ch') ? 'Ch.' : 'Ep.';
        console.debug(`parseVersionKey: matched standalone chapter/episode: ${displayType} ${number}`);
        return { key: 1 + number / 100, valueStr: `${displayType} ${number}` };
    }

    // Version pattern (e.g., "v1.2")
    const versionRegex = /v(?:ersion)?[\s._-]?(\d[\d.]*)/i;
    match = cleanedText.match(versionRegex);
    if (match && match[1]) {
        console.debug(`parseVersionKey: matched version: v${match[1]}`);
        return { key: parseFloat(match[1]), valueStr: `v${match[1]}` };
    }

    console.debug('parseVersionKey: no version matched for text:', text);
    return null;
}

function createCompressedVersionLink(label, href) {
    const link = document.createElement('a');
    link.className = 'f95-compressed-version-link'; // Important for skip detection
    link.href = href;
    link.textContent = label;
    link.target = '_blank';
    link.rel = 'noopener noreferrer';
    link.style.cssText = 'display: inline-block; margin: 10px 0 12px 0; padding: 8px 14px; background-color: #ba4545; color: white; font-weight: bold; font-size: 14px; text-decoration: none; border-radius: 4px; cursor: pointer;';
    link.title = 'Go to compressed version post';
    return link;
}

function insertLinkAboveDownloadDiv(firstPost, element) {
    const candidates = firstPost.querySelectorAll('div[style*="text-align: center"]');
    for (const div of candidates) {
        if (div.textContent.trim().toUpperCase().includes('DOWNLOAD')) {
            div.parentNode.insertBefore(element, div);
            return true;
        }
    }
    const firstSpoiler = firstPost.querySelector('.bbCodeSpoiler');
    if (firstSpoiler) {
        firstSpoiler.parentNode.insertBefore(element, firstSpoiler);
    } else {
        firstPost.insertBefore(element, firstPost.firstChild);
    }
    return false;
}

function isGameReleasePost(post) {
    if (!post) return false;
    const centerDivs = post.querySelectorAll('div[style*="text-align: center"]');
    for (const div of centerDivs) {
        if (div.textContent.trim().toUpperCase().includes('DOWNLOAD')) return true;
    }
    const spoilerButtons = post.querySelectorAll('.bbCodeSpoiler-button');
    for (const button of spoilerButtons) {
        const buttonText = button.textContent.trim().toUpperCase();
        if (buttonText.includes('DOWNLOAD') || buttonText.includes('LINKS')) return true;
    }
    return false;
}

// Conservative quote/request filter:
// - Avoid treating bare "please"/"pls" as automatic request.
// - Still filter short polite "compressed version please" or negative-download phrases like "I won't download 7GB".
function looksLikeQuoteOrRequest(text) {
    if (!text) return true;
    const s = text.trim().toLowerCase();


    // Too short — likely a small reply or single-line comment
    if (s.length < 15) {
        console.debug('looksLikeQuoteOrRequest: text too short, filtering out:', text);
        return true;
    }

    // Common indicators of quotes/requests/questions
    if (s.includes('said:') || s.startsWith('quote') || s.startsWith('spoiler') ||
        s.includes('request') || s.includes('looking for') || s.includes('anyone have') || s.endsWith('?')) {
        console.debug('looksLikeQuoteOrRequest: text looks like quote/request, filtering out:', text);
        return true;
    }

    // Page-reference / "see page X of this thread" — usually replies pointing elsewhere, not original release posts
    if (/\bon page\s+(?:one|\d+)\b/i.test(s) || /\bpage\s+(?:one|\d+)\s+of (?:this )?thread\b/i.test(s) || /\bpage\s+one\b/i.test(s)) {
        console.debug('looksLikeQuoteOrRequest: page-reference detected, filtering out:', text);
        return true;
    }

    // Polite short requests like "compressed version please" — require proximity between "compressed" and politeness token
    const politeTokens = '(?:please|pls|if possible|would be great)';
    const compressedToken = '\\bcompressed(?: version)?\\b';
    const politeNearCompressed = new RegExp(compressedToken + '[\\s\\S]{0,60}' + politeTokens, 'i').test(text) ||
          new RegExp(politeTokens + '[\\s\\S]{0,60}' + compressedToken, 'i').test(text);
    if (politeNearCompressed) {
        console.debug('looksLikeQuoteOrRequest: polite compressed request detected (nearby), filtering out:', text);
        return true;
    }

    // Negative download phrasing — indicates a request or complaint (won't/can't download)
    if (/\b(not (gonna|going to)|won't|will not|cannot|can't|dont want to|don't want to|too big|too large|too big for)\b/.test(s) &&
        /\b(download|dl|gb|mb|gbs|gb)\b/.test(s)) {
        console.debug('looksLikeQuoteOrRequest: negative download phrasing detected, filtering out:', text);
        return true;
    }

    // List-style or enumerations (1) or bullets
    if (/\b\d+\)\s/.test(s) || /\b\d+\.\s/.test(s) || /•/.test(s) || /- \[?\s?\]/.test(s)) {
        console.debug('looksLikeQuoteOrRequest: detected list/enum style content, filtering out:', text);
        return true;
    }

    // Short request phrases (deliberately exclude generic 'please'/'pls' here)
    const requestPhrases = [
        'i just need',
        'just need',
        'i need',
        'need a',
        'need an',
        'i want',
        'want a',
        'looking to',
        'anyone got',
        'anyone has',
        'is there a compressed',
        'where can i find',
        'where can i go',
        'does anyone have',
        'anyone have',
        'looking for a compressed',
        'looking for compressed'
    ];
    for (const p of requestPhrases) {
        if (s.includes(p)) {
            console.debug('looksLikeQuoteOrRequest: matched request phrase, filtering out:', p, 'in', text);
            return true;
        }
    }

    return false;
}

function parseHTMLWithBase(html) {
    const doc = new DOMParser().parseFromString(html, 'text/html');
    const base = document.createElement('base');
    base.href = window.location.origin;
    if (doc.head) {
        doc.head.appendChild(base);
    } else {
        const head = document.createElement('head');
        head.appendChild(base);
        doc.insertBefore(head, doc.firstChild);
    }
    return doc;
}

/* Tentative & Technical announcement detection */

const KNOWN_HOSTS = ['mega','pixeldrain','workupload','buzzheavier','datanodes','akirabox','gofile','uploadhaven','mixdrop','vikingfile','gofile.io','zippyshare'];

function isTentativeAnnouncement(text) {
    if (!text) return false;
    const s = text.toLowerCase();


    // If there's an explicit URL or link tag, treat as concrete (not tentative)
    if (/\bhttps?:\/\/[^\s"'<>]+/i.test(s) || /\[(url|code|spoiler)/i.test(s)) return false;

    const technicalPatterns = [
        /filegetter::compressed/i,
        /\bcompressed\s*=/i,
        /\bwriting_list\b/i,
        /\bwr iting_list\b/i, // sometimes spaced typo
        /\bjoiplay\b/i,
        /\bjoi ?play\b/i,
        /\bapk\b/i,
        /\bandroid\b/i,
        /\bcompanion stat\b/i
    ];
    if (technicalPatterns.some(re => re.test(s))) {
        return true;
    }

    const tentativePatterns = [
        /\bmay make (?:available|available through|available on|available via|available in)\b/i,
        /\bmay (?:be available|make available|upload|post)\b/i,
        /\bmight (?:make|be|upload|release|post)\b/i,
        /\bmaybe (?:make|be|upload|release|post)\b/i,
        /\bprobably (?:make|be|upload|release|post)\b/i,
        /\bdoubt it\b/i,
        /\bdon('?|’?)t (?:really )?have the time\b/i,
        /\b(i(?:'m| am)? )?consider(?:ing)?\b/i,
        /\bi(?:'ll| will)? (?:see|try|maybe|might)\b/i
    ];
    for (const re of tentativePatterns) {
        if (re.test(s)) return true;
    }

    const hostAlternation = KNOWN_HOSTS.map(h => h.replace(/\./g, '\\.')).join('|');
    const tentativeHostRegex = new RegExp(`\\b(?:may|might|maybe|probably|could)\\b[\\s\\S]{0,80}\\b(?:make|be|upload|post|available)\\b[\\s\\S]{0,40}\\b(?:${hostAlternation})\\b`, 'i');
    if (tentativeHostRegex.test(s)) return true;

    return false;
}

/* skip Mods forum threads helper */
function isInModsForum() {
    const crumbs = [...document.querySelectorAll('.p-breadcrumbs a')];
    for (const a of crumbs) {
        const href = a.getAttribute('href') || '';
        const txt = (a.textContent || '').toLowerCase();
        if (href.includes('/forums/mods.') || txt.includes('mods')) return true;
    }
    return false;
}

/* Compression-discussion negative filter */
function isCompressionDiscussion(text) {
    if (!text) return false;
    const s = text.toLowerCase();


    const negativePhrases = [
        'already compressed',
        'already quite compressed',
        'already at its maximum',
        'max compression',
        'maximum compression',
        'nothing we can do',
        'not compressible',
        'can\'t compress',
        'cannot compress',
        'no further compress',
        'won\'t compress',
        'will make av angry',
        'will make av',
        'makes av angry',
        'we use .webp',
        'we use .ogg',
        'we use webp',
        'we use ogg',
        'use .webp',
        'use .ogg',
        'webp for pictures',
        'ogg for the music',
        'text-files are not compressed',
        'data is already quite compressed'
    ];

    for (const p of negativePhrases) {
        if (s.includes(p)) {
            console.debug('isCompressionDiscussion: matched negative phrase:', p);
            return true;
        }
    }

    if (/\balready (?:compressed|quite compressed|at (?:its )?maximum)\b/i.test(s)) {
        console.debug('isCompressionDiscussion: regex matched "already compressed" style phrase');
        return true;
    }
    if (/\buses?\.?\s*(webp|ogg|mp3|wav|flac|jpg|jpeg|png)\b/i.test(s)) {
        console.debug('isCompressionDiscussion: matched uses <format> pattern');
        return true;
    }

    // Technical/troubleshooting markers — e.g., the post about freezing / rpg_core.js / skipCount / framerate
    if (/\brpg(?:\s*maker|maker mv)?\b/i.test(s) ||
        /\brpg_core\.js\b/i.test(s) ||
        /\bskipcount\b/i.test(s) ||
        /\bframerate\b|\bfps\b/i.test(s) ||
        /\bfreeze(?:d|s)?\b|\balt\s*f4\b|\baltf4\b|\bcrash(?:es)?\b/i.test(s) ||
        /\bruntime error\b|\berror log\b/i.test(s)) {
        console.debug('isCompressionDiscussion: technical/troubleshooting content detected, filtering out:', text);
        return true;
    }

    return false;
}

/* findBestMatchingCompressedPost with improved host/archive detection */
function findBestMatchingCompressedPost(html) {
    const doc = parseHTMLWithBase(html);
    const items = [...doc.querySelectorAll('li.block-row.js-inlineModContainer')];


    console.debug(`findBestMatchingCompressedPost: Found ${items.length} search result items`);

    if (items.length === 0) return null;

    const compressedRegex = /(compressed|compress|archive|\.zip|\.rar|\.7z)/i;

    // Strong release indicator regex: explicit archive ext, URL, or well-known host (with optional common TLD)
    const strongHostPattern = [
        '\\.zip\\b',
        '\\.rar\\b',
        '\\.7z\\b',
        'https?:\\/\\/[^\s"\'<>]+',
        '\\b(?:mega(?:\\.nz)?|gofile(?:\\.io)?|pixeldrain|workupload|zippyshare|uploadhaven|mixdrop|vikingfile|akirabox|datanodes|buzzheavier)\\b'
    ].join('|');
    const strongReleaseRegex = new RegExp(strongHostPattern, 'i');

    // match sizes like "5GB" or "700 MB"
    const sizeRegex = /\b\d+(?:[\.,]\d+)?\s*(GB|MB|G|M)\b/i;
    // Exclude plain "download" — it's often used in requests ("I won't download 7GB") and is not a reliable positive indicator.
    const sizeActionRegex = /\b(?:dl|upload|link|links|mirror|mirrors|host|uploaded|uploader)\b/i;

    const hasLinkRegex = /\[(URL|CODE|SPOILER)/i;
    const urlInTextRegex = /https?:\/\/[^\s"'<>]+/gi;
    const shortCompressedQuestionRegex = /^compressed( version)?\??$/i;

    const sizeNegativeWords = ['limit', 'download limit', 'per day', 'daily', 'quota', 'cap', 'bandwidth', 'speed', 'monthly', 'rate limit', 'throttle', 'limit of', 'per ip', 'per account', 'limit u', 'limit you'];

    let candidates = items.map((li, index) => {
        const postLinkElem = li.querySelector('.contentRow-main h3.contentRow-title a[href]');
        if (!postLinkElem) {
            console.debug(`[${index}] Missing postLinkElem - skipping`);
            return null;
        }
        const postUrl = postLinkElem.href;

        const snippetElem = li.querySelector('.contentRow-main .contentRow-snippet');
        const snippetText = snippetElem ? snippetElem.textContent.trim() : '';
        console.debug(`[${index}] Snippet:`, snippetText);

        // Skip snippets that explicitly point to another page of this thread (commonly a reply)
        if ((/\bon page\s+(?:one|\d+)\b.*\bof (?:this )?thread\b/i.test(snippetText)) ||
            (/\bon page\s+(?:one|\d+)\b/i.test(snippetText) && /\b(of|of this)\b/i.test(snippetText))) {
            console.debug(`[${index}] References another page of this thread (e.g. "On page X") - skipping`);
            return null;
        }

        if (!compressedRegex.test(snippetText)) {
            console.debug(`[${index}] Does not mention compression - skipping`);
            return null;
        }

        if (looksLikeQuoteOrRequest(snippetText)) {
            console.debug(`[${index}] Looks like quote/request - skipping`);
            return null;
        }

        if (shortCompressedQuestionRegex.test(snippetText)) {
            console.debug(`[${index}] Snippet is short compressed version question - skipping`);
            return null;
        }

        const hasLinkTag = hasLinkRegex.test(snippetText);
        const urlsInText = snippetText.match(urlInTextRegex) || [];

        const sizeMatch = sizeRegex.exec(snippetText);
        const actionMatch = sizeActionRegex.exec(snippetText);
        let sizeActionNear = false;
        if (sizeMatch && actionMatch) {
            const threshold = 60;
            sizeActionNear = Math.abs(actionMatch.index - sizeMatch.index) <= threshold;
        }

        let sizeContextNegativeNear = false;
        if (sizeMatch) {
            const sizeIndex = sizeMatch.index;
            const windowSize = 60;
            const start = Math.max(0, sizeIndex - windowSize);
            const end = Math.min(snippetText.length, sizeIndex + (sizeMatch[0] ? sizeMatch[0].length + windowSize : windowSize));
            const surrounding = snippetText.slice(start, end).toLowerCase();
            for (const w of sizeNegativeWords) {
                if (surrounding.includes(w)) {
                    sizeContextNegativeNear = true;
                    console.debug(`[${index}] Size context negative word found near size: "${w}"`);
                    break;
                }
            }
        }

        // Use the stricter "strongReleaseRegex" only.
        const hasStrongReleaseIndicator = strongReleaseRegex.test(snippetText);

        // hasHost only if a strong indicator OR size+action near each other and not negative
        const hasHost = hasStrongReleaseIndicator || (sizeMatch && actionMatch && sizeActionNear && !sizeContextNegativeNear);

        const candidateVersionInfo = parseVersionKey(snippetText);

        const lowerSnippet = snippetText.toLowerCase();
        if (/\bandroid\b/i.test(lowerSnippet) || /\bport\b/i.test(lowerSnippet) || /\bapk\b/i.test(lowerSnippet)) {
            console.debug(`[${index}] mentions android/port/apk - skipping`);
            return null;
        }

        // Compression-discussion negative filter: skip if it's purely a compression discussion and no strong indicators/version/link
        if (isCompressionDiscussion(snippetText) && !candidateVersionInfo && !hasLinkTag && !hasStrongReleaseIndicator) {
            console.debug(`[${index}] Compression discussion only (no links/version/strong host) - skipping`);
            return null;
        }

        if (isTentativeAnnouncement(snippetText) && urlsInText.length === 0 && !hasLinkTag && !candidateVersionInfo) {
            console.debug(`[${index}] Tentative/technical announcement without links/version - skipping`);
            return null;
        }

        if (!(candidateVersionInfo || hasLinkTag || hasHost)) {
            console.debug(`[${index}] Lacks version/link/release indicator - skipping`);
            return null;
        }

        const timeElem = li.querySelector('time.u-dt');
        if (!timeElem) {
            console.debug(`[${index}] Missing time element - skipping`);
            return null;
        }
        const postDate = new Date(timeElem.getAttribute('datetime'));
        if (isNaN(postDate.getTime())) {
            console.debug(`[${index}] Invalid date - skipping`);
            return null;
        }

        console.debug(`[${index}] Candidate found: URL=${postUrl}, Date=${postDate.toISOString()}, VersionInfo=${candidateVersionInfo ? JSON.stringify(candidateVersionInfo) : 'N/A'}, hasHost=${hasHost}, hasLinkTag=${hasLinkTag}`);

        return {
            url: postUrl,
            date: postDate,
            versionInfo: candidateVersionInfo,
            hasReleaseIndicator: hasHost,
            hasLinkTag,
        };
    }).filter(Boolean);

    if (candidates.length === 0) {
        console.debug('No valid candidates found.');
        return null;
    }

    candidates.sort((a, b) => {
        if (!!b.versionInfo && !a.versionInfo) return 1;
        if (!!a.versionInfo && !b.versionInfo) return -1;
        if (b.hasReleaseIndicator && !a.hasReleaseIndicator) return 1;
        if (a.hasReleaseIndicator && !b.hasReleaseIndicator) return -1;
        return b.date - a.date;
    });

    console.debug('Top candidate selected:', candidates[0]);
    return candidates[0];
}

function postSearchUsingPageForm(searchQuery, threadId, page = 1) {
    return new Promise((resolve, reject) => {
        const forms = document.querySelectorAll('form[action="/search/search"]');
        let searchForm = null;
        for (const form of forms) {
            if (form.querySelector('select[name="constraints"]')) {
                searchForm = form;
                break;
            }
        }
        if (!searchForm) {
            reject(new Error("Could not find search form with 'constraints' dropdown."));
            return;
        }
        const formData = new FormData(searchForm);
        formData.set('keywords', searchQuery);
        if (threadId) formData.set('thread_ids', threadId);
        formData.set('search_in', 'posts');
        if (page > 1) formData.set('page', page);


        const params = new URLSearchParams();
        for (const pair of formData.entries()) {
            params.append(pair[0], pair[1]);
        }

        const postUrl = new URL(searchForm.action, window.location.href).href;

        console.debug(`Sending search request for page ${page} to ${postUrl}`);
        GM_xmlhttpRequest({
            method: "POST",
            url: postUrl,
            data: params.toString(),
            headers: {
                "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8",
                "User-Agent": navigator.userAgent,
                "Referer": window.location.href
            },
            onload: r => {
                if (r.status >= 200 && r.status < 300) {
                    console.debug(`Search page ${page} loaded (${r.responseText.length} bytes)`);
                    resolve(r.responseText);
                } else {
                    reject(new Error(`HTTP Error ${r.status}: ${r.statusText}`));
                }
            },
            onerror: () => reject(new Error("Network error during search")),
        });
    });
}

async function trySearchAllPages(searchQuery, threadId, maxPages = 3) {
    let bestCandidate = null;
    for (let page = 1; page <= maxPages; page++) {
        try {
            console.debug(`Attempting search on page ${page}`);
        const html = await postSearchUsingPageForm(searchQuery, threadId, page);
        const candidate = findBestMatchingCompressedPost(html);


        if (candidate) {
            if (!bestCandidate) {
                bestCandidate = candidate;
                console.debug(`Best candidate initialized from page ${page}:`, candidate);
            } else {
                if (!!candidate.versionInfo && !bestCandidate.versionInfo) {
                    bestCandidate = candidate;
                    console.debug(`Best candidate replaced (versionInfo priority) from page ${page}:`, candidate);
                } else if (!candidate.versionInfo && bestCandidate.versionInfo) {
                    console.debug(`Skipped candidate from page ${page} - lower versionInfo priority`);
                } else if (candidate.hasReleaseIndicator && !bestCandidate.hasReleaseIndicator) {
                    bestCandidate = candidate;
                    console.debug(`Best candidate replaced (release indicator priority) from page ${page}:`, candidate);
                } else if (!candidate.hasReleaseIndicator && bestCandidate.hasReleaseIndicator) {
                    console.debug(`Skipped candidate from page ${page} - lower releaseIndicator priority`);
                } else if (candidate.date > bestCandidate.date) {
                    bestCandidate = candidate;
                    console.debug(`Best candidate replaced (newer date) from page ${page}:`, candidate);
                } else {
                    console.debug(`Skipped candidate from page ${page} - older or equal date`);
                }
            }
        } else {
            console.debug(`No candidate found on page ${page}`);
        }

        if (page === 1 && bestCandidate?.versionInfo && bestCandidate.hasReleaseIndicator) {
            console.debug('Early break: High-quality candidate found on page 1');
            break;
        }
    } catch (err) {
        console.error(`Error during search on page ${page}:`, err);
        break;
    }
}
return bestCandidate;
}

window.addEventListener('load', () => {
    (async function main() {
        try {
            console.debug('F95 Compressed Version Link Script started.');


            if (isInModsForum()) {
                console.debug('In Mods forum - skipping script.');
                return;
            }

            const firstPost = document.querySelector('.message-threadStarterPost.message--post.js-post');
            if (!firstPost) {
                console.debug('No first post found, exiting.');
                return;
            }
            if (!isGameReleasePost(firstPost)) {
                console.debug('First post does not appear to be a game release post; exiting.');
                return;
            }

            if (firstPost.querySelector('.f95-compressed-version-link')) {
                console.debug('Compressed version link already present; skipping.');
                return;
            }

            const urlMatch = window.location.pathname.match(/\.([0-9]+)(\/|$)/);
            if (!urlMatch) {
                console.debug('Cannot extract thread ID; exiting.');
                return;
            }
            const threadId = urlMatch[1];
            const searchQuery = 'compressed';

            const candidate = await trySearchAllPages(searchQuery, threadId, 3);

            if (!candidate) {
                console.debug('No matching compressed version candidate found.');
                const warn = document.createElement('span');
                warn.textContent = 'No matching compressed version found.';
                warn.style.cssText = 'color:#ba4545; display:block; margin:10px 0 12px;';
                insertLinkAboveDownloadDiv(firstPost, warn);
                return;
            }

            console.debug('Final compressed version candidate:', candidate);
            const versionStr = candidate.versionInfo ? candidate.versionInfo.valueStr : 'Link';
            const linkLabel = `Compressed Version (${versionStr})`;
            const link = createCompressedVersionLink(linkLabel, candidate.url);
            insertLinkAboveDownloadDiv(firstPost, link);

        } catch (ex) {
            console.error('F95 Compressed Version Script Error:', ex);
        }
    })();
});
1752706894678.png
P.S. Bug report for this script should contain game thread link and script version and make sure that you read about know bugs first.
 
Last edited:

master861

Well-Known Member
Nov 4, 2022
1,245
1,604
Are you going to update rpakit and rpatool?Because renpy 8.3.x and higher are using rpa 4 and both tools only are wokring with rpa 3 and lower.
 

megalol

Engaged Member
Modder
Compressor
Respected User
Apr 3, 2017
2,809
11,732
Are you going to update rpakit and rpatool?Because renpy 8.3.x and higher are using rpa 4 and both tools only are wokring with rpa 3 and lower.
No, unless u make proper bug report with game examples since nobody reported any problems before with it and I've never heard about rpa v4 so maybe it's just some custom made rpa type by some developer (I've tested with latest SDK v8.4.0 and it still creates v3 rpa that was compressed and packed back to rpa by UAGC just fine with default settings, game works fine after compression).
 
  • Like
Reactions: Discoplayer1

master861

Well-Known Member
Nov 4, 2022
1,245
1,604
Yes i checked it again its are custom rpa archive and the game is to small for compression and only for one game making are custom tool.If more game using custom rpa's then maybe it is worth but for now i dont thing so.I take are look if i can change that for me.Its based on are rpa 3 .

class RPA40(RPA3):
"""A slightly custom variant of RPA-3.0."""

name = "RPA-4.0"
header = b"RPA-4.0"
 
Last edited:

megalol

Engaged Member
Modder
Compressor
Respected User
Apr 3, 2017
2,809
11,732
(install at UAGC v3.1.3 v13):
For RenPy (main, not bulk) compressor added support for AV1 NVENC video encoder (make sure first that your GPU and RenPy engine version supports AV1). Check compressed video size feature now checks if compressed videos size at least 10% smaller than original.
 

EVIL555

Member
Oct 27, 2022
184
116
(install at UAGC v3.1.3 v13):
For RenPy (main, not bulk) compressor added support for AV1 NVENC video encoder (make sure first that your GPU and RenPy engine version supports AV1). Check compressed video size feature now checks if compressed videos size at least 10% smaller than original.
and how to update?
 

megalol

Engaged Member
Modder
Compressor
Respected User
Apr 3, 2017
2,809
11,732
no need compress.
I think game dev does good job "packing" this game what it is size which is 1,6gb.
tested (godot vulkan compress which it started), but we know the results so. [End of story]
my compress did opposite 3,11gb. :LUL::BootyTime::Kappa:
Probably it's new Godot version without res:// inside pck file paths (u can check pck file with , choose ui version) so if u didn't enabled no res:// feature in UAGC then u'll get almost twice bigger pck file. Also u need to pack the game (pck) after compression to 7z/rar/zip or size would be almost the same anyway.
 
  • Like
Reactions: Discoplayer1

Kybo

Active Member
Compressor
Mar 27, 2022
970
1,902
Probably it's new Godot version without res:// inside pck file paths (u can check pck file with , choose ui version) so if u didn't enabled no res:// feature in UAGC then u'll get almost twice bigger pck file. Also u need to pack the game (pck) after compression to 7z/rar/zip or size would be almost the same anyway.
its the tool latest version??, doesn´t seems to keep open that i can look at it pck files. my pc is 64 ui
took micro second picture while usin tool lol
1753190609181.png
 
Last edited:

megalol

Engaged Member
Modder
Compressor
Respected User
Apr 3, 2017
2,809
11,732
its the tool latest version??, doesn´t seems to keep open that i can look at it pck files. my pc is 64 ui
took micro second picture while usin tool lol
View attachment 5066803
file is on
D: though
testing copypaste -> throw to C:
UI version requires thats why it don't work for u. Tool is working fine, I've even checked those Godot game and it's "no res" one.
1753191563072.png
here is other game (with "res://") for comparison:
1753191706983.png
 

Kybo

Active Member
Compressor
Mar 27, 2022
970
1,902
UI version requires thats why it don't work for u. Tool is working fine, I've even checked those Godot game and it's "no res" one.
edit: got it open finally
1753192760721.png
thx
 
Last edited:

megalol

Engaged Member
Modder
Compressor
Respected User
Apr 3, 2017
2,809
11,732
Well i can´t do much about .res files. no tutorials which are Enabled.res .reses (.ctex files??) and which not. non.res files exported .red ??

(Godot Engine)

yes i can with the Godot PCK Explorer extracted files saw the whole files which are in ít in extracted folder game file
View attachment 5067157
and can extract pack with pck files or "packed" version.

Only question is which are
  • exported .red (non res??)
  • imported .ctex (enabled res??)
gonna try tomorrow again
View attachment 5067078
https://f95zone.to/threads/honest-bond-v0-09f-public-hard-bone-games.231720/
its the game
Lol, I've just suggested u to enable 1 button in UAGC, not reverse game files :), info about opening pck and see if there are res:// inside or not was needed for enabling dat feature at screenshot below or not (in ur case it's rly needs to be enabled)...
1753199422013.png
 
  • Like
Reactions: Discoplayer1

Kybo

Active Member
Compressor
Mar 27, 2022
970
1,902
1753199646508.png
need to download newest version got UAGC_v3.1.3 <-> v127z
(was gonna ask magic button anyway / no "res-pack button)

thx
 
5.00 star(s) 3 Votes