Full Code of Citrinate/FreePackages for AI

main 042a89c9e47a cached
113 files
1.5 MB
702.9k tokens
300 symbols
1 requests
Download .txt
Showing preview only (1,657K chars total). Download the full file or copy to clipboard to get everything.
Repository: Citrinate/FreePackages
Branch: main
Commit: 042a89c9e47a
Files: 113
Total size: 1.5 MB

Directory structure:
gitextract_ow9uday2/

├── .editorconfig
├── .gitattributes
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   ├── feature_request.md
│   │   └── question.md
│   ├── RELEASE_TEMPLATE.md
│   └── workflows/
│       └── publish.yml
├── .gitignore
├── .gitmodules
├── Directory.Build.props
├── Directory.Packages.props
├── FreePackages/
│   ├── .gitignore
│   ├── Commands.cs
│   ├── Data/
│   │   ├── Cache/
│   │   │   ├── BotCache.cs
│   │   │   └── GlobalCache.cs
│   │   ├── External/
│   │   │   ├── ASFInfo.cs
│   │   │   └── CardApps.cs
│   │   └── PICS/
│   │       └── ProductInfo.cs
│   ├── FreePackages.cs
│   ├── FreePackages.csproj
│   ├── Handlers/
│   │   ├── PICSHandler.cs
│   │   ├── PackageHandler.cs
│   │   └── SteamHandler.cs
│   ├── Helpers/
│   │   ├── DeterministicHasher.cs
│   │   └── StatusReporter.cs
│   ├── IPC/
│   │   ├── Api/
│   │   │   └── FreePackagesController.cs
│   │   ├── Requests/
│   │   │   └── QueueLicensesRequest.cs
│   │   └── Responses/
│   │       └── FreeSubResponse.cs
│   ├── Json.cs
│   ├── Localization/
│   │   ├── README.md
│   │   ├── Strings.de-DE.resx
│   │   ├── Strings.resx
│   │   ├── Strings.ru-RU.resx
│   │   ├── Strings.tr-TR.resx
│   │   ├── Strings.uk-UA.resx
│   │   ├── Strings.zh-Hans.resx
│   │   └── Strings.zh-Hant.resx
│   ├── PackageFilter/
│   │   ├── FilterConfig.cs
│   │   ├── Filterables/
│   │   │   ├── FilterableApp.cs
│   │   │   └── FilterablePackage.cs
│   │   └── PackageFilter.cs
│   ├── PackageQueue/
│   │   ├── ActivationQueue.cs
│   │   ├── Package.cs
│   │   ├── PackageQueue.cs
│   │   └── RemovalQueue.cs
│   └── WebRequest.cs
├── FreePackages.Tests/
│   ├── Apps.cs
│   ├── Filters.cs
│   ├── FreePackages.Tests.csproj
│   ├── Packages.cs
│   ├── TestData/
│   │   ├── app_which_is_free.txt
│   │   ├── app_with_categories.txt
│   │   ├── app_with_content_descriptors.txt
│   │   ├── app_with_deck_playable.txt
│   │   ├── app_with_deck_unknown.txt
│   │   ├── app_with_deck_unsupported.txt
│   │   ├── app_with_deck_verified.txt
│   │   ├── app_with_dlc.txt
│   │   ├── app_with_language_support.txt
│   │   ├── app_with_purchase_restricted_countries.txt
│   │   ├── app_with_release_state.txt
│   │   ├── app_with_required_app.txt
│   │   ├── app_with_restricted_countries.txt
│   │   ├── app_with_review_score.txt
│   │   ├── app_with_state.txt
│   │   ├── app_with_tags.txt
│   │   ├── app_with_type.txt
│   │   ├── demo_which_will_be_removed.txt
│   │   ├── demo_with_fewer_categories_than_parent.txt
│   │   ├── demo_with_fewer_categories_than_parent_parent.txt
│   │   ├── demo_with_fewer_content_descriptors_than_parent.txt
│   │   ├── demo_with_fewer_content_descriptors_than_parent_parent.txt
│   │   ├── demo_with_fewer_languages_than_parent.txt
│   │   ├── demo_with_fewer_languages_than_parent_parent.txt
│   │   ├── demo_with_fewer_tags_than_parent.txt
│   │   ├── demo_with_fewer_tags_than_parent_parent.txt
│   │   ├── package_which_is_free.txt
│   │   ├── package_which_is_no_cost.txt
│   │   ├── package_with_deactivated_demo.txt
│   │   ├── package_with_demo_which_will_be_removed.txt
│   │   ├── package_with_disallowed_app.txt
│   │   ├── package_with_free_weekend.txt
│   │   ├── package_with_purchase_restricted_countries.txt
│   │   ├── package_with_restricted_countries.txt
│   │   ├── package_with_single_app.txt
│   │   ├── package_with_single_app_app_1.txt
│   │   ├── package_with_timed_activation.txt
│   │   ├── playtest_with_hidden_parent.txt
│   │   ├── playtest_with_hidden_parent_parent.txt
│   │   ├── playtest_with_no_categories.txt
│   │   ├── playtest_with_no_categories_parent.txt
│   │   ├── playtest_with_no_languages.txt
│   │   ├── playtest_with_no_languages_parent.txt
│   │   ├── playtest_with_no_waitlist.txt
│   │   ├── playtest_with_no_waitlist_parent.txt
│   │   ├── userdata_empty.json
│   │   ├── userdata_with_excluded_content_descriptors.json
│   │   ├── userdata_with_excluded_tags.json
│   │   ├── userdata_with_followed_apps.json
│   │   ├── userdata_with_ignored_apps.json
│   │   ├── userdata_with_wishlist_apps.json
│   │   └── userinfo_empty.json
│   └── generate_test_data.sh
├── FreePackages.sln
├── FreePackagesImporter/
│   ├── README.md
│   └── code.user.js
├── LICENSE
├── README.md
├── SECURITY.md
├── build.bat
├── build.sh
├── crowdin.yml
└── github-pandoc.css

================================================
FILE CONTENTS
================================================

================================================
FILE: .editorconfig
================================================
root = true

[*]
indent_style = tab
insert_final_newline = true
trim_trailing_whitespace = true

[*.cs]
csharp_prefer_braces = true:suggestion
csharp_new_line_before_open_brace = none:suggestion
csharp_new_line_before_else = false:suggestion
csharp_new_line_before_catch = false:suggestion
csharp_new_line_before_finally = false:suggestion
csharp_new_line_before_members_in_object_initializers = false:suggestion
csharp_new_line_before_members_in_anonymous_types = false:suggestion
csharp_new_line_between_query_expression_clauses = false:suggestion

csharp_style_var_for_built_in_types = false:suggestion
csharp_style_var_when_type_is_apparent = false:suggestion
csharp_style_var_elsewhere = false:suggestion

csharp_style_expression_bodied_methods = when_on_single_line:suggestion
csharp_style_expression_bodied_constructors = when_on_single_line:suggestion
csharp_style_expression_bodied_operators = when_on_single_line:suggestion
csharp_style_expression_bodied_properties = when_on_single_line:suggestion
csharp_style_expression_bodied_indexers = when_on_single_line:suggestion
csharp_style_expression_bodied_accessors = when_on_single_line:suggestion

csharp_style_pattern_matching_over_as_with_null_check = false:suggestion

csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion

csharp_preserve_single_line_statements = false:suggestion
csharp_preserve_single_line_blocks = true:suggestion

csharp_indent_case_contents = true:suggestion
csharp_indent_switch_labels = true:suggestion

csharp_space_after_cast = true:suggestion
csharp_space_after_keywords_in_control_flow_statements = true:suggestion
csharp_space_between_method_declaration_parameter_list_parentheses = false:suggestion
csharp_space_between_method_call_parameter_list_parentheses = false:suggestion
csharp_space_between_parentheses = false:suggestion
csharp_space_before_colon_in_inheritance_clause = true:suggestion
csharp_space_after_colon_in_inheritance_clause = true:suggestion
csharp_space_around_binary_operators = before_and_after:suggestion
csharp_space_between_method_declaration_empty_parameter_list_parentheses = false:suggestion
csharp_space_between_method_call_name_and_opening_parenthesis = false:suggestion
csharp_space_between_method_call_empty_parameter_list_parentheses = false:suggestion


dotnet_sort_system_directives_first = true:suggestion

dotnet_style_require_accessibility_modifiers = always:suggestion
dotnet_style_qualification_for_field = false:suggestion
dotnet_style_qualification_for_property = false:suggestion
dotnet_style_qualification_for_method = false:suggestion
dotnet_style_qualification_for_event = false:suggestion
dotnet_style_prefer_auto_properties = true:suggestion
dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion
dotnet_style_predefined_type_for_member_access = true:suggestion

# PascalCase for naming everything but parameters
dotnet_naming_rule.all_members_must_be_capitalized.symbols = all_symbols
dotnet_naming_symbols.all_symbols.applicable_kinds = class,struct,interface,enum,property,method,field,event,delegate
dotnet_naming_symbols.all_symbols.applicable_accessibilities = *
dotnet_naming_rule.all_members_must_be_capitalized.style = all_symbols
dotnet_naming_style.all_symbols.capitalization = pascal_case
dotnet_naming_rule.all_members_must_be_capitalized.severity = suggestion

# camelCase for naming parameters
dotnet_naming_rule.parameters_must_be_camel_case.symbols = params
dotnet_naming_symbols.params.applicable_kinds = parameter
dotnet_naming_symbols.params.applicable_accessibilities = *
dotnet_naming_rule.parameters_must_be_camel_case.style = params
dotnet_naming_style.params.capitalization = camel_case
dotnet_naming_rule.parameters_must_be_camel_case.severity = suggestion


================================================
FILE: .gitattributes
================================================
# Auto detect text files and perform LF normalization
* text=auto

*.sh text eol=lf

# Custom for Visual Studio
*.cs diff=csharp


================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.md
================================================
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''

---

**Describe the bug**
A clear and concise description of what the bug is.

**Plugin Version**
What version of the plugin are you using.

**ArchiSteamFarm Version and Variant**
What version of ArchiSteamFarm are you using and do you use ASF-generic or some other variant.

**Logs**
Provide the logs from when the bug occurred.  You can find these in the `log.txt` file directly in the ASF directory.


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.md
================================================
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''

---




================================================
FILE: .github/ISSUE_TEMPLATE/question.md
================================================
---
name: Question
about: Ask a question about the project
title: ''
labels: question
assignees: ''

---




================================================
FILE: .github/RELEASE_TEMPLATE.md
================================================
This version requires ArchiSteamFarm VX.X.X.X or newer

### Changelog

- 


================================================
FILE: .github/workflows/publish.yml
================================================
name: publish

on: [push, pull_request]

env:
  PLUGIN_NAME: "FreePackages"
  DOTNET_SDK_VERSION: 10.0

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v4.1.1
        with:
          submodules: recursive

      - name: Setup .NET Core
        uses: actions/setup-dotnet@v4.0.0
        with:
          dotnet-version: ${{ env.DOTNET_SDK_VERSION }}

      - name: Verify .NET Core
        run: dotnet --info

      - name: Prepare for publishing
        run: dotnet restore

      - name: Run FreePackages.Tests
        run: dotnet test FreePackages.Tests -p:ContinuousIntegrationBuild=true --nologo

      - name: Publish
        run: |
          dotnet publish ${{ env.PLUGIN_NAME }} -c "Release" -o "out/generic" -p:ContinuousIntegrationBuild=true --nologo
          mkdir -p ./out/dist/${{ env.PLUGIN_NAME }}
          cp ./out/generic/${{ env.PLUGIN_NAME }}.dll ./out/dist/${{ env.PLUGIN_NAME }}
          ( cd ./out/generic/ ; cp --parents ./*/${{ env.PLUGIN_NAME }}.resources.dll ../dist/${{ env.PLUGIN_NAME }} || : )

      - name: Create README
        uses: docker://pandoc/core:3.1
        with:
          args: --metadata title="${{ env.PLUGIN_NAME }}" --standalone --columns 2000 -f markdown -t html --embed-resources --standalone -c ./github-pandoc.css -o ./out/dist/${{ env.PLUGIN_NAME }}/README.html README.md

      - name: Upload ${{ env.PLUGIN_NAME }}
        uses: actions/upload-artifact@v4.0.0
        with:
          name: ${{ env.PLUGIN_NAME }}
          path: out/dist/${{ env.PLUGIN_NAME }}

  release:
    if: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags/') }}
    needs: publish
    runs-on: ubuntu-latest

    permissions:
      contents: write
      attestations: write
      id-token: write

    steps:
      - name: Checkout code
        uses: actions/checkout@v4.1.1

      - name: Download ${{ env.PLUGIN_NAME }} artifact
        uses: actions/download-artifact@v4.1.7
        with:
          name: ${{ env.PLUGIN_NAME }}
          path: out

      - name: Create Zip
        run: |
          cd out
          7z a -tzip -mx7 ${{ env.PLUGIN_NAME }}.zip *

      - name: Generate artifact attestation
        uses: actions/attest-build-provenance@v2
        with:
          subject-path: 'out/${{ env.PLUGIN_NAME }}.zip'

      - name: Create ${{ env.PLUGIN_NAME }} GitHub release
        uses: ncipollo/release-action@v1.13.0
        with:
          artifacts: out/${{ env.PLUGIN_NAME }}.zip
          artifactContentType: application/zip
          name: ${{ env.PLUGIN_NAME }} V${{ github.ref_name }}
          tag: ${{ github.ref_name }}
          bodyFile: .github/RELEASE_TEMPLATE.md
          token: ${{ secrets.GITHUB_TOKEN }}
          makeLatest: false
          prerelease: true
          draft: true


================================================
FILE: .gitignore
================================================
#     _     ____   _____
#    / \   / ___| |  ___|
#   / _ \  \___ \ | |_
#  / ___ \  ___) ||  _|
# /_/   \_\|____/ |_|

# Ignore all files in custom in-tree config directory (if exists)
ArchiSteamFarm/config

# Ignore local log + debug of development builds
ArchiSteamFarm/log.txt
ArchiSteamFarm/debug

# Ignore standard out folders for publishing
**/out

# Ignore crowdin CLI secret (if exists)
tools/ArchiCrowdin/crowdin_identity.yml

#  _      _
# | |    (_) _ __   _   _ __  __
# | |    | || '_ \ | | | |\ \/ /
# | |___ | || | | || |_| | >  <
# |_____||_||_| |_| \__,_|/_/\_\
#
# https://github.com/github/gitignore/blob/master/Global/Linux.gitignore

*~

# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*

# KDE directory preferences
.directory

# Linux trash folder which might appear on any partition or disk
.Trash-*

# .nfs files are created when an open file is removed but is still being accessed
.nfs*

#                           ___   ____
#  _ __ ___    __ _   ___  / _ \ / ___|
# | '_ ` _ \  / _` | / __|| | | |\___ \
# | | | | | || (_| || (__ | |_| | ___) |
# |_| |_| |_| \__,_| \___| \___/ |____/
#
# https://github.com/github/gitignore/blob/master/Global/macOS.gitignore

# General
.DS_Store
.AppleDouble
.LSOverride

# Icon must end with two \r
Icon

# Thumbnails
._*

# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent

# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk

#  __  __                       ____                     _
# |  \/  |  ___   _ __    ___  |  _ \   ___ __   __ ___ | |  ___   _ __
# | |\/| | / _ \ | '_ \  / _ \ | | | | / _ \\ \ / // _ \| | / _ \ | '_ \
# | |  | || (_) || | | || (_) || |_| ||  __/ \ V /|  __/| || (_) || |_) |
# |_|  |_| \___/ |_| |_| \___/ |____/  \___|  \_/  \___||_| \___/ | .__/
#                                                                 |_|
#
# https://github.com/github/gitignore/blob/master/Global/MonoDevelop.gitignore

#User Specific
*.userprefs
*.usertasks

#Mono Project Files
*.pidb
*.resources
test-results/

# __     __ _                     _  ____   _               _  _
# \ \   / /(_) ___  _   _   __ _ | |/ ___| | |_  _   _   __| |(_)  ___
#  \ \ / / | |/ __|| | | | / _` || |\___ \ | __|| | | | / _` || | / _ \
#   \ V /  | |\__ \| |_| || (_| || | ___) || |_ | |_| || (_| || || (_) |
#    \_/   |_||___/ \__,_| \__,_||_||____/  \__| \__,_| \__,_||_| \___/
#
# https://github.com/github/gitignore/blob/master/VisualStudio.gitignore

## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore

# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates

# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs

# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/

# Visual Studio 2015/2017 cache/options directory
.vs/
.vscode/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/

# Visual Studio 2017 auto generated files
Generated\ Files/

# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*

# NUNIT
*.VisualState.xml
TestResult.xml

# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c

# Benchmark Results
BenchmarkDotNet.Artifacts/

# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/

# StyleCop
StyleCopReport.xml

# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc

# Chutzpah Test files
_Chutzpah*

# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb

# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap

# Visual Studio Trace Files
*.e2e

# TFS 2012 Local Workspace
$tf/

# Guidance Automation Toolkit
*.gpState

# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user

# JustCode is a .NET coding add-in
.JustCode

# TeamCity is a build add-in
_TeamCity*

# DotCover is a Code Coverage Tool
*.dotCover

# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json

# Visual Studio code coverage results
*.coverage
*.coveragexml

# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*

# MightyMoose
*.mm.*
AutoTest.Net/

# Web workbench (sass)
.sass-cache/

# Installshield output folder
[Ee]xpress/

# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html

# Click-Once directory
publish/

# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj

# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/

# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets

# Microsoft Azure Build Output
csx/
*.build.csdef

# Microsoft Azure Emulator
ecf/
rcf/

# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx

# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/

# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs

# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk

# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# ASP.NET Core default setup: bower directory is configured as wwwroot/lib/ and bower restore is true
**/wwwroot/lib/

# RIA/Silverlight projects
Generated_Code/

# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak

# SQL Server files
*.mdf
*.ldf
*.ndf

# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser

# Microsoft Fakes
FakesAssemblies/

# GhostDoc plugin setting file
*.GhostDoc.xml

# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/

# Visual Studio 6 build log
*.plg

# Visual Studio 6 workspace options file
*.opt

# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw

# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions

# Paket dependency manager
.paket/paket.exe
paket-files/

# FAKE - F# Make
.fake/

# JetBrains Rider
.idea/
*.sln.iml

# CodeRush personal settings
.cr/personal

# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc

# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config

# Tabs Studio
*.tss

# Telerik's JustMock configuration file
*.jmconfig

# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs

# OpenCover UI analysis results
OpenCover/

# Azure Stream Analytics local run output
ASALocalRun/

# MSBuild Binary and Structured Log
*.binlog

# NVidia Nsight GPU debugger configuration file
*.nvuser

# MFractors (Xamarin productivity tool) working folder
.mfractor/

# Local History for Visual Studio
.localhistory/

# BeatPulse healthcheck temp database 
healthchecksdb

# __     __ _                     _  ____   _               _  _          ____            _
# \ \   / /(_) ___  _   _   __ _ | |/ ___| | |_  _   _   __| |(_)  ___   / ___| ___    __| |  ___
#  \ \ / / | |/ __|| | | | / _` || |\___ \ | __|| | | | / _` || | / _ \ | |    / _ \  / _` | / _ \
#   \ V /  | |\__ \| |_| || (_| || | ___) || |_ | |_| || (_| || || (_) || |___| (_) || (_| ||  __/
#    \_/   |_||___/ \__,_| \__,_||_||____/  \__| \__,_| \__,_||_| \___/  \____|\___/  \__,_| \___|
#
# https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore

.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json

# __        __ _             _
# \ \      / /(_) _ __    __| |  ___ __      __ ___
#  \ \ /\ / / | || '_ \  / _` | / _ \\ \ /\ / // __|
#   \ V  V /  | || | | || (_| || (_) |\ V  V / \__ \
#    \_/\_/   |_||_| |_| \__,_| \___/  \_/\_/  |___/
#
# https://github.com/github/gitignore/blob/master/Global/Windows.gitignore

# Windows thumbnail cache files
Thumbs.db
ehthumbs.db
ehthumbs_vista.db

# Dump file
*.stackdump

# Folder config file
[Dd]esktop.ini

# Recycle Bin used on file shares
$RECYCLE.BIN/

# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp

# Windows shortcuts
*.lnk

# https://www.paraesthesia.com/archive/2022/09/30/strongly-typed-resources-with-net-core/
*.Designer.cs


================================================
FILE: .gitmodules
================================================
[submodule "ArchiSteamFarm"]
	path = ArchiSteamFarm
	url = https://github.com/JustArchiNET/ArchiSteamFarm.git


================================================
FILE: Directory.Build.props
================================================
<Project>
	<Import Project="ArchiSteamFarm/Directory.Build.props" />

	<PropertyGroup>
		<PluginName>FreePackages</PluginName>
		<Version>1.6.3.3</Version>
	</PropertyGroup>

	<PropertyGroup>
		<ApplicationIcon />
		<Authors>Citrinate</Authors>
		<Company>$(Authors)</Company>
		<Copyright>Copyright © $([System.DateTime]::UtcNow.Year) $(Company)</Copyright>
		<Description>$(PluginName) description.</Description>
		<PackageIcon />
		<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
		<PackageProjectUrl>https://github.com/$(Company)/$(PluginName)</PackageProjectUrl>
		<PackageReleaseNotes>$(PackageProjectUrl)/releases</PackageReleaseNotes>
		<RepositoryUrl>$(PackageProjectUrl).git</RepositoryUrl>
	</PropertyGroup>

	<!-- TODO: Fix warnings in the code, then we can treat warnings as error -->
	<PropertyGroup>
		<TreatWarningsAsErrors>false</TreatWarningsAsErrors>
	</PropertyGroup>

	<!-- Reset ASF signing settings, as we'll use our own logic -->
	<PropertyGroup>
		<AssemblyOriginatorKeyFile />
		<PublicSign>false</PublicSign>
		<SignAssembly>false</SignAssembly>
	</PropertyGroup>

	<!-- Enable public signing, if provided with public key -->
	<PropertyGroup Condition="'$(Configuration)' == 'Release' AND EXISTS('resources/$(PluginName).snk.pub')">
		<AssemblyOriginatorKeyFile>../resources/$(PluginName).snk.pub</AssemblyOriginatorKeyFile>
		<PublicSign>true</PublicSign>
		<SignAssembly>true</SignAssembly>
	</PropertyGroup>

	<!-- Private SNK signing, if provided with secret -->
	<PropertyGroup Condition="'$(Configuration)' == 'Release' AND EXISTS('resources/$(PluginName).snk')">
		<AssemblyOriginatorKeyFile>../resources/$(PluginName).snk</AssemblyOriginatorKeyFile>
		<PublicSign>false</PublicSign>
		<SignAssembly>true</SignAssembly>
	</PropertyGroup>
</Project>


================================================
FILE: Directory.Packages.props
================================================
<Project>
	<Import Project="ArchiSteamFarm/Directory.Packages.props" />
</Project>


================================================
FILE: FreePackages/.gitignore
================================================
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore

# User-specific files
*.suo
*.user
*.userosscache
*.sln.docstates

# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs

# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
**/out

# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/

# Visual Studio 2017 auto generated files
Generated\ Files/

# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*

# NUNIT
*.VisualState.xml
TestResult.xml

# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c

# Benchmark Results
BenchmarkDotNet.Artifacts/

# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
**/Properties/launchSettings.json

# StyleCop
StyleCopReport.xml

# Files built by Visual Studio
*_i.c
*_p.c
*_i.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*.log
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
*.zip

# Chutzpah Test files
_Chutzpah*

# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb

# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap

# Visual Studio Trace Files
*.e2e

# TFS 2012 Local Workspace
$tf/

# Guidance Automation Toolkit
*.gpState

# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user

# JustCode is a .NET coding add-in
.JustCode

# TeamCity is a build add-in
_TeamCity*

# DotCover is a Code Coverage Tool
*.dotCover

# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json

# Visual Studio code coverage results
*.coverage
*.coveragexml

# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*

# MightyMoose
*.mm.*
AutoTest.Net/

# Web workbench (sass)
.sass-cache/

# Installshield output folder
[Ee]xpress/

# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html

# Click-Once directory
publish/

# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj

# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/

# NuGet Packages
*.nupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets

# Microsoft Azure Build Output
csx/
*.build.csdef

# Microsoft Azure Emulator
ecf/
rcf/

# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx

# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!*.[Cc]ache/

# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs

# Including strong name files can present a security risk 
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk

# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/

# RIA/Silverlight projects
Generated_Code/

# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak

# SQL Server files
*.mdf
*.ldf
*.ndf

# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser

# Microsoft Fakes
FakesAssemblies/

# GhostDoc plugin setting file
*.GhostDoc.xml

# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/

# Visual Studio 6 build log
*.plg

# Visual Studio 6 workspace options file
*.opt

# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw

# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions

# Paket dependency manager
.paket/paket.exe
paket-files/

# FAKE - F# Make
.fake/

# JetBrains Rider
.idea/
*.sln.iml

# CodeRush
.cr/

# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc

# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config

# Tabs Studio
*.tss

# Telerik's JustMock configuration file
*.jmconfig

# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs

# OpenCover UI analysis results
OpenCover/

# Azure Stream Analytics local run output 
ASALocalRun/

# MSBuild Binary and Structured Log
*.binlog

# NVidia Nsight GPU debugger configuration file
*.nvuser

# MFractors (Xamarin productivity tool) working folder 
.mfractor/


================================================
FILE: FreePackages/Commands.cs
================================================
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using AngleSharp.Dom;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Steam;
using FreePackages.Localization;

namespace FreePackages {
	internal static class Commands {
		internal static async Task<string?> Response(Bot bot, EAccess access, ulong steamID, string message, string[] args) {
			if (!Enum.IsDefined(access)) {
				throw new InvalidEnumArgumentException(nameof(access), (int) access, typeof(EAccess));
			}

			if (string.IsNullOrEmpty(message)) {
				return null;
			}

			switch (args.Length) {
				case 1:
					switch (args[0].ToUpperInvariant()) {
						case "FREEPACKAGES" when access >= EAccess.Master:
							return String.Format("{0} {1}", nameof(FreePackages), (typeof(FreePackages).Assembly.GetName().Version ?? new Version("0")).ToString());

						case "CANCELREMOVE" or "CANCELREMOVAL":
							return ResponseCancelRemove(bot, access);

						case "CONFIRMREMOVE" or "CONFIRMREMOVAL":
							return ResponseConfirmRemove(bot, access);

						case "CLEARQUEUE" or "CLEARFREEPACKAGESQUEUE":
							return ResponseClearQueue(bot, access);

						case "QSA":
							return ResponseQueueStatus(access, steamID, "ASF");
						case "QSTATUS" or "QUEUESTATUS":
							return ResponseQueueStatus(bot, access);

						case "REMOVEFREEPACKAGES":
							return await ResponseRemoveFreePackages(bot, access, new StatusReporter(bot, steamID)).ConfigureAwait(false);

						case "REMOVEFREEPACKAGES^":
							return await ResponseRemoveFreePackages(bot, access, new StatusReporter(bot, steamID), excludePlayed: true).ConfigureAwait(false);

						case "REMOVEALLFREEPACKAGES":
							return await ResponseRemoveFreePackages(bot, access, new StatusReporter(bot, steamID), removeAll: true).ConfigureAwait(false);

						case "REMOVEALLFREEPACKAGES^":
							return await ResponseRemoveFreePackages(bot, access, new StatusReporter(bot, steamID), removeAll: true, excludePlayed: true).ConfigureAwait(false);

						default:
							return null;
					};
				default:
					switch (args[0].ToUpperInvariant()) {
						case "CANCELREMOVE" or "CANCELREMOVAL":
							return ResponseCancelRemove(access, steamID, args[1]);

						case "CONFIRMREMOVE" or "CONFIRMREMOVAL":
							return ResponseConfirmRemove(access, steamID, args[1]);

						case "CLEARQUEUE" or "CLEARFREEPACKAGESQUEUE":
							return ResponseClearQueue(access, steamID, args[1]);

						case "DONTREMOVE" when args.Length > 2:
							return ResponseDontRemove(access, steamID, args[1], Utilities.GetArgsAsText(args, 2, ","));
						case "DONTREMOVE":
							return ResponseDontRemove(bot, access, args[1]);

						case "QSTATUS" or "QUEUESTATUS":
							return ResponseQueueStatus(access, steamID, args[1]);

						case "QLICENSE" or "QUEUELICENSE" or "QLICENCE" or "QUEUELICENCE" when args.Length > 2:
							return ResponseQueueLicense(access, steamID, args[1], Utilities.GetArgsAsText(args, 2, ","));
						case "QLICENSE" or "QUEUELICENSE" or "QLICENCE" or "QUEUELICENCE" :
							return ResponseQueueLicense(bot, access, args[1]);

						case "QLICENSE^" or "QUEUELICENSE^" or "QLICENCE^" or "QUEUELICENCE^" when args.Length > 2:
							return ResponseQueueLicense(access, steamID, args[1], Utilities.GetArgsAsText(args, 2, ","), useFilter: true);
						case "QLICENSE^" or "QUEUELICENSE^" or "QLICENCE^" or "QUEUELICENCE^" :
							return ResponseQueueLicense(bot, access, args[1], useFilter: true);

						case "REMOVEFREEPACKAGES":
							return await ResponseRemoveFreePackages(access, steamID, args[1], new StatusReporter(bot, steamID)).ConfigureAwait(false);

						case "REMOVEFREEPACKAGES^":
							return await ResponseRemoveFreePackages(access, steamID, args[1], new StatusReporter(bot, steamID), excludePlayed: true).ConfigureAwait(false);

						case "REMOVEALLFREEPACKAGES":
							return await ResponseRemoveFreePackages(access, steamID, args[1], new StatusReporter(bot, steamID), removeAll: true).ConfigureAwait(false);

						case "REMOVEALLFREEPACKAGES^":
							return await ResponseRemoveFreePackages(access, steamID, args[1], new StatusReporter(bot, steamID), removeAll: true, excludePlayed: true).ConfigureAwait(false);

						default:
							return null;
					}
			}
		}

		private static string? ResponseCancelRemove(Bot bot, EAccess access) {
			if (access < EAccess.Master) {
				return null;
			}

			if (!bot.IsConnectedAndLoggedOn) {
				return FormatBotResponse(bot, ArchiSteamFarm.Localization.Strings.BotNotConnected);
			}

			if (!PackageHandler.Handlers.Keys.Contains(bot.BotName)) {
				return FormatBotResponse(bot, Strings.PluginNotEnabled);
			}

			return FormatBotResponse(bot, PackageHandler.Handlers[bot.BotName].CancelRemoval());
		}

		private static string? ResponseCancelRemove(EAccess access, ulong steamID, string botNames) {
			if (String.IsNullOrEmpty(botNames)) {
				throw new ArgumentNullException(nameof(botNames));
			}

			HashSet<Bot>? bots = Bot.GetBots(botNames);

			if ((bots == null) || (bots.Count == 0)) {
				return access >= EAccess.Owner ? FormatStaticResponse(String.Format(ArchiSteamFarm.Localization.Strings.BotNotFound, botNames)) : null;
			}

			IEnumerable<string?> results = bots.Select(bot => ResponseCancelRemove(bot, ArchiSteamFarm.Steam.Interaction.Commands.GetProxyAccess(bot, access, steamID)));

			List<string?> responses = new(results.Where(result => !String.IsNullOrEmpty(result)));

			return responses.Count > 0 ? String.Join(Environment.NewLine, responses) : null;
		}

		private static string? ResponseConfirmRemove(Bot bot, EAccess access) {
			if (access < EAccess.Master) {
				return null;
			}

			if (!bot.IsConnectedAndLoggedOn) {
				return FormatBotResponse(bot, ArchiSteamFarm.Localization.Strings.BotNotConnected);
			}

			if (!PackageHandler.Handlers.Keys.Contains(bot.BotName)) {
				return FormatBotResponse(bot, Strings.PluginNotEnabled);
			}

			return FormatBotResponse(bot, PackageHandler.Handlers[bot.BotName].ConfirmRemoval());
		}

		private static string? ResponseConfirmRemove(EAccess access, ulong steamID, string botNames) {
			if (String.IsNullOrEmpty(botNames)) {
				throw new ArgumentNullException(nameof(botNames));
			}

			HashSet<Bot>? bots = Bot.GetBots(botNames);

			if ((bots == null) || (bots.Count == 0)) {
				return access >= EAccess.Owner ? FormatStaticResponse(String.Format(ArchiSteamFarm.Localization.Strings.BotNotFound, botNames)) : null;
			}

			IEnumerable<string?> results = bots.Select(bot => ResponseConfirmRemove(bot, ArchiSteamFarm.Steam.Interaction.Commands.GetProxyAccess(bot, access, steamID)));

			List<string?> responses = new(results.Where(result => !String.IsNullOrEmpty(result)));

			return responses.Count > 0 ? String.Join(Environment.NewLine, responses) : null;
		}

		private static string? ResponseClearQueue(Bot bot, EAccess access) {
			if (access < EAccess.Master) {
				return null;
			}

			if (!bot.IsConnectedAndLoggedOn) {
				return FormatBotResponse(bot, ArchiSteamFarm.Localization.Strings.BotNotConnected);
			}

			if (!PackageHandler.Handlers.Keys.Contains(bot.BotName)) {
				return FormatBotResponse(bot, Strings.PluginNotEnabled);
			}

			return FormatBotResponse(bot, PackageHandler.Handlers[bot.BotName].ClearQueue());
		}

		private static string? ResponseClearQueue(EAccess access, ulong steamID, string botNames) {
			if (String.IsNullOrEmpty(botNames)) {
				throw new ArgumentNullException(nameof(botNames));
			}

			HashSet<Bot>? bots = Bot.GetBots(botNames);

			if ((bots == null) || (bots.Count == 0)) {
				return access >= EAccess.Owner ? FormatStaticResponse(String.Format(ArchiSteamFarm.Localization.Strings.BotNotFound, botNames)) : null;
			}

			IEnumerable<string?> results = bots.Select(bot => ResponseClearQueue(bot, ArchiSteamFarm.Steam.Interaction.Commands.GetProxyAccess(bot, access, steamID)));

			List<string?> responses = new(results.Where(result => !String.IsNullOrEmpty(result)));

			return responses.Count > 0 ? String.Join(Environment.NewLine, responses) : null;
		}

		private static string? ResponseDontRemove(Bot bot, EAccess access, string licenses) {
			if (access < EAccess.Master) {
				return null;
			}

			if (!bot.IsConnectedAndLoggedOn) {
				return FormatBotResponse(bot, ArchiSteamFarm.Localization.Strings.BotNotConnected);
			}

			if (!PackageHandler.Handlers.Keys.Contains(bot.BotName)) {
				return FormatBotResponse(bot, Strings.PluginNotEnabled);
			}

			// https://github.com/JustArchiNET/ArchiSteamFarm/blob/d972c93072dd8d2bf0f2cecda3561dc3ba77a9ed/ArchiSteamFarm/Steam/Interaction/Commands.cs#L626C3-L626C34
			StringBuilder response = new();

			string[] entries = licenses.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);

			foreach (string entry in entries) {
				uint gameID;
				string type;

				int index = entry.IndexOf('/', StringComparison.Ordinal);

				if ((index > 0) && (entry.Length > index + 1)) {
					if (!uint.TryParse(entry[(index + 1)..], out gameID) || (gameID == 0)) {
						response.AppendLine(FormatBotResponse(bot, string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.ErrorIsInvalid, nameof(gameID))));

						continue;
					}

					type = entry[..index];
				} else if (uint.TryParse(entry, out gameID) && (gameID > 0)) {
					type = "SUB";
				} else {
					response.AppendLine(FormatBotResponse(bot, string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.ErrorIsInvalid, nameof(gameID))));

					continue;
				}

				EPackageType packageType;
				type = type.ToUpperInvariant();
				if (type == "A" || type == "APP") {
					packageType = EPackageType.RemoveApp;
				} else {
					packageType = EPackageType.RemoveSub;
				}

				response.AppendLine(FormatBotResponse(bot, PackageHandler.Handlers[bot.BotName].ModifyRemovables(packageType, gameID)));
			}

			return response.Length > 0 ? response.ToString() : null;
		}

		private static string? ResponseDontRemove(EAccess access, ulong steamID, string botNames, string licenses) {
			if (String.IsNullOrEmpty(botNames)) {
				throw new ArgumentNullException(nameof(botNames));
			}

			HashSet<Bot>? bots = Bot.GetBots(botNames);

			if ((bots == null) || (bots.Count == 0)) {
				return access >= EAccess.Owner ? FormatStaticResponse(String.Format(ArchiSteamFarm.Localization.Strings.BotNotFound, botNames)) : null;
			}

			IEnumerable<string?> results = bots.Select(bot => ResponseDontRemove(bot, ArchiSteamFarm.Steam.Interaction.Commands.GetProxyAccess(bot, access, steamID), licenses));

			List<string?> responses = new(results.Where(result => !String.IsNullOrEmpty(result)));

			Utilities.InBackground(async() => await PackageHandler.HandleChanges().ConfigureAwait(false));

			return responses.Count > 0 ? String.Join(Environment.NewLine, responses) : null;
		}

		private static string? ResponseQueueStatus(Bot bot, EAccess access) {
			if (access < EAccess.Master) {
				return null;
			}

			if (!bot.IsConnectedAndLoggedOn) {
				return FormatBotResponse(bot, ArchiSteamFarm.Localization.Strings.BotNotConnected);
			}

			if (!PackageHandler.Handlers.Keys.Contains(bot.BotName)) {
				return FormatBotResponse(bot, Strings.PluginNotEnabled);
			}

			return FormatBotResponse(bot, PackageHandler.Handlers[bot.BotName].GetStatus());
		}

		private static string? ResponseQueueStatus(EAccess access, ulong steamID, string botNames) {
			if (String.IsNullOrEmpty(botNames)) {
				throw new ArgumentNullException(nameof(botNames));
			}

			HashSet<Bot>? bots = Bot.GetBots(botNames);

			if ((bots == null) || (bots.Count == 0)) {
				return access >= EAccess.Owner ? FormatStaticResponse(String.Format(ArchiSteamFarm.Localization.Strings.BotNotFound, botNames)) : null;
			}

			IEnumerable<string?> results = bots.Select(bot => ResponseQueueStatus(bot, ArchiSteamFarm.Steam.Interaction.Commands.GetProxyAccess(bot, access, steamID)));

			List<string?> responses = new(results.Where(result => !String.IsNullOrEmpty(result)));

			return responses.Count > 0 ? String.Join(Environment.NewLine, responses) : null;
		}

		private static string? ResponseQueueLicense(Bot bot, EAccess access, string licenses, bool useFilter = false, [CallerMemberName] string? previousMethodName = null) {
			if (access < EAccess.Master) {
				return null;
			}

			if (!bot.IsConnectedAndLoggedOn) {
				return FormatBotResponse(bot, ArchiSteamFarm.Localization.Strings.BotNotConnected);
			}

			if (!PackageHandler.Handlers.Keys.Contains(bot.BotName)) {
				return FormatBotResponse(bot, Strings.PluginNotEnabled);
			}

			// https://github.com/JustArchiNET/ArchiSteamFarm/blob/d972c93072dd8d2bf0f2cecda3561dc3ba77a9ed/ArchiSteamFarm/Steam/Interaction/Commands.cs#L626C3-L626C34
			StringBuilder response = new();

			string[] entries = licenses.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries);

			foreach (string entry in entries) {
				uint gameID;
				string type;

				int index = entry.IndexOf('/', StringComparison.Ordinal);

				if ((index > 0) && (entry.Length > index + 1)) {
					if (!uint.TryParse(entry[(index + 1)..], out gameID) || (gameID == 0)) {
						response.AppendLine(FormatBotResponse(bot, string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.ErrorIsInvalid, nameof(gameID))));

						continue;
					}

					type = entry[..index];
				} else if (uint.TryParse(entry, out gameID) && (gameID > 0)) {
					type = "SUB";
				} else {
					response.AppendLine(FormatBotResponse(bot, string.Format(CultureInfo.CurrentCulture, ArchiSteamFarm.Localization.Strings.ErrorIsInvalid, nameof(gameID))));

					continue;
				}

				EPackageType packageType;
				type = type.ToUpperInvariant();
				if (type == "A" || type == "APP") {
					packageType = EPackageType.App;
				} else {
					packageType = EPackageType.Sub;
				}

				response.AppendLine(FormatBotResponse(bot, PackageHandler.Handlers[bot.BotName].AddPackage(packageType, gameID, useFilter)));
			}

			if (previousMethodName == nameof(Response)) {
				Utilities.InBackground(async() => await PackageHandler.HandleChanges().ConfigureAwait(false));
			}

			return response.Length > 0 ? response.ToString() : null;
		}

		private static string? ResponseQueueLicense(EAccess access, ulong steamID, string botNames, string licenses, bool useFilter = false) {
			if (String.IsNullOrEmpty(botNames)) {
				throw new ArgumentNullException(nameof(botNames));
			}

			HashSet<Bot>? bots = Bot.GetBots(botNames);

			if ((bots == null) || (bots.Count == 0)) {
				return access >= EAccess.Owner ? FormatStaticResponse(String.Format(ArchiSteamFarm.Localization.Strings.BotNotFound, botNames)) : null;
			}

			IEnumerable<string?> results = bots.Select(bot => ResponseQueueLicense(bot, ArchiSteamFarm.Steam.Interaction.Commands.GetProxyAccess(bot, access, steamID), licenses, useFilter));

			List<string?> responses = new(results.Where(result => !String.IsNullOrEmpty(result)));

			Utilities.InBackground(async() => await PackageHandler.HandleChanges().ConfigureAwait(false));

			return responses.Count > 0 ? String.Join(Environment.NewLine, responses) : null;
		}

		private static async Task<string?> ResponseRemoveFreePackages(Bot bot, EAccess access, StatusReporter statusReporter, bool excludePlayed = false, bool removeAll = false) {
			if (access < EAccess.Master) {
				return null;
			}

			if (!bot.IsConnectedAndLoggedOn) {
				return FormatBotResponse(bot, ArchiSteamFarm.Localization.Strings.BotNotConnected);
			}

			if (!PackageHandler.Handlers.Keys.Contains(bot.BotName)) {
				return FormatBotResponse(bot, Strings.PluginNotEnabled);
			}

			IDocument? accountLicensesPage = await WebRequest.GetAccountLicenses(bot);
			if (accountLicensesPage == null) {
				return FormatBotResponse(bot, Strings.LicensePageFetchFail);
			}

			Regex removablePackageIDsRegex = new Regex("RemoveFreeLicense\\(\\s*(?<subID>[0-9]+),\\s*'(?<encodedName>[A-Za-z0-9+/=]*)'", RegexOptions.CultureInvariant); // matches the parameters of: RemoveFreeLicense( 45946, 'UmV2ZXJzaW9uOiBUaGUgRXNjYXBl' );
			MatchCollection removablePackageMatches = removablePackageIDsRegex.Matches(accountLicensesPage.Source.Text);
			if (removablePackageMatches.Count == 0) {
				return FormatBotResponse(bot, Strings.LicensePageEmpty);
			}

			Dictionary<uint, string> removeablePackages = new();
			foreach (Match match in removablePackageMatches) {
				string name;
				try {
					name = Encoding.UTF8.GetString(Convert.FromBase64String(match.Groups["encodedName"].Value));
				} catch (Exception e) {
					bot.ArchiLogger.LogGenericException(e);

					return FormatBotResponse(bot, String.Format(ArchiSteamFarm.Localization.Strings.ErrorParsingObject, "encodedName"));
				}

				string subIDString = match.Groups["subID"].Value;
				if (!uint.TryParse(subIDString, out uint subID)) {
					return FormatBotResponse(bot, String.Format(ArchiSteamFarm.Localization.Strings.ErrorParsingObject, "subID"));
				}

				removeablePackages[subID] = name;				
			}

			Utilities.InBackground(
				async() => {
					await PackageHandler.Handlers[bot.BotName].ScanRemovables(removeablePackages, excludePlayed, removeAll, statusReporter).ConfigureAwait(false);
				}
			);

			int removableScanTimeEstimateMinutes = (int) Math.Round(2.5 * ((double) removeablePackages.Count / ProductInfo.ItemsPerProductInfoRequest) * ((double) ProductInfo.ProductInfoLimitingDelaySeconds / 60));

			return FormatBotResponse(bot, String.Format(Strings.RemovalWaitMessage, removableScanTimeEstimateMinutes, String.Format("!cancelremove {0}", bot.BotName)));
		}

		private static async Task<string?> ResponseRemoveFreePackages(EAccess access, ulong steamID, string botName, StatusReporter statusReporter, bool excludePlayed = false, bool removeAll = false) {
			if (String.IsNullOrEmpty(botName)) {
				throw new ArgumentNullException(nameof(botName));
			}

			Bot? bot = Bot.GetBot(botName);

			if (bot == null) {
				return access >= EAccess.Owner ? FormatStaticResponse(String.Format(ArchiSteamFarm.Localization.Strings.BotNotFound, botName)) : null;
			}

			return await ResponseRemoveFreePackages(bot, ArchiSteamFarm.Steam.Interaction.Commands.GetProxyAccess(bot, access, steamID), statusReporter, excludePlayed, removeAll).ConfigureAwait(false);
		}

		internal static string FormatStaticResponse(string response) => ArchiSteamFarm.Steam.Interaction.Commands.FormatStaticResponse(response);
		internal static string FormatBotResponse(Bot bot, string response) => bot.Commands.FormatBotResponse(response);
	}
}

================================================
FILE: FreePackages/Data/Cache/BotCache.cs
================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using ArchiSteamFarm.Collections;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Helpers;
using ArchiSteamFarm.Helpers.Json;
using SteamKit2;

namespace FreePackages {
	internal sealed class BotCache : SerializableFile {
		[JsonInclude]
		[JsonDisallowNull]
		internal ConcurrentHashSet<Package> Packages { get; private set; } = new(new PackageComparer());

		[JsonInclude]
		[JsonDisallowNull]
		internal ConcurrentHashSet<DateTime> Activations { get; private set; } = new();

		[JsonInclude]
		[JsonDisallowNull]
		internal ConcurrentHashSet<uint> ChangedApps { get; private set; } = new();

		[JsonInclude]
		[JsonDisallowNull]
		internal ConcurrentHashSet<uint> ChangedPackages { get; private set; } = new();

		[JsonInclude]
		[JsonDisallowNull]
		internal ConcurrentHashSet<uint> NewOwnedPackages { get; private set; } = new();

		[JsonInclude]
		[JsonDisallowNull]
		internal ConcurrentHashSet<uint> SeenPackages { get; private set; } = new();

		[JsonInclude]
		[JsonDisallowNull]
		internal ConcurrentHashSet<uint> WaitlistedPlaytests { get; private set; } = new();

		[JsonInclude]
		[JsonDisallowNull]
		internal ConcurrentHashSet<uint> IgnoredApps { get; private set; } = new();

		private HashSet<uint> SeenPackageIDActivations = new();
		private readonly object LockObject = new();

		[JsonConstructor]
		internal BotCache() { }

		internal BotCache(string filePath) : this() {
			if (string.IsNullOrEmpty(filePath)) {
				throw new ArgumentNullException(nameof(filePath));
			}

			FilePath = filePath;
		}

		protected override Task Save() => Save(this);

		internal static async Task<BotCache?> CreateOrLoad(string filePath) {
			if (string.IsNullOrEmpty(filePath)) {
				throw new ArgumentNullException(nameof(filePath));
			}

			if (!File.Exists(filePath)) {
				return new BotCache(filePath);
			}

			BotCache? botCache;
			try {
				string json = await File.ReadAllTextAsync(filePath).ConfigureAwait(false);

				if (string.IsNullOrEmpty(json)) {
					ASF.ArchiLogger.LogGenericError(string.Format(ArchiSteamFarm.Localization.Strings.ErrorIsEmpty, nameof(json)));

					return null;
				}

				botCache = json.ToJsonObject<BotCache>();
			} catch (Exception e) {
				ASF.ArchiLogger.LogGenericException(e);

				return null;
			}

			if (botCache == null) {
				ASF.ArchiLogger.LogNullError(botCache);

				return null;
			}

			botCache.Packages = new(botCache.Packages.GroupBy(package => package, new PackageComparer()).Select(group => group.First()), new PackageComparer());
			botCache.FilePath = filePath;
			
			return botCache;
		}

		internal bool AddPackage(Package package) {
			if (Packages.Contains(package)) {
				return false;
			}

			Packages.Add(package);
			Utilities.InBackground(Save);

			return true;
		}

		internal bool AddPackages(IEnumerable<Package> packages) {
			if (!packages.Except(Packages).Any()) {
				// There are no new packages to add
				return false;
			}

			Packages.UnionWith(packages);
			Utilities.InBackground(Save);

			return true;
		}

		internal bool RemovePackage(Package package) {
			Packages.Remove(package);
			Utilities.InBackground(Save);

			return true;
		}

		internal bool RemoveAppPackages(HashSet<uint> appIDsToRemove) {
			Packages.Where(x => x.Type == EPackageType.App && appIDsToRemove.Contains(x.ID)).ToList().ForEach(x => Packages.Remove(x));
			Utilities.InBackground(Save);

			return true;
		}

		internal Package? GetNextPackage(HashSet<EPackageType> types) {
			// Return the package which should be activated first, prioritizing first packages which have a start and end date
			ulong now = DateUtils.DateTimeToUnixTime(DateTime.UtcNow);
			Package? package = Packages.FirstOrDefault(x => x.StartTime != null && now > x.StartTime && types.Contains(x.Type));
			if (package != null) {
				return package;
			}

			return Packages.FirstOrDefault(x => x.StartTime == null && types.Contains(x.Type));
		}

		internal void AddActivation(DateTime activation, uint count = 1, IReadOnlyCollection<uint>? packageIDs = null) {
			var activationsToPrune = Activations.Where(x => x < DateTime.Now.AddMinutes(-1 * ActivationQueue.ActivationPeriodMinutes)).ToList();
			if (activationsToPrune.Count > 0) {
				activationsToPrune.ForEach(x => Activations.Remove(x));
			}

			lock(LockObject) {
				int numUnseenPackageActivations = packageIDs?.Where(packageID => !SeenPackageIDActivations.Contains(packageID)).Count() ?? 0;
				if (packageIDs == null || numUnseenPackageActivations > 0) {
					if (packageIDs != null) {
						SeenPackageIDActivations.UnionWith(packageIDs);
					}

					for (int i = 0; i < Math.Max(count, numUnseenPackageActivations); i++) {
						Activations.Add(activation.AddSeconds(-1 * i));
					}
				}
			}

			Utilities.InBackground(Save);
		}

		internal int NumActivationsPastPeriod() {
			return Activations.Where(activation => activation > DateTime.Now.AddMinutes(-1 * ActivationQueue.ActivationPeriodMinutes)).Count();
		}

		internal DateTime? GetLastActivation() {
			// Can't use Activations.Max() because it's missing on non-generic ASF
			DateTime? lastActivation = null;
			foreach (DateTime activation in Activations) {
				if (lastActivation == null || activation > lastActivation) {
					lastActivation = activation;
				}
			}

			return lastActivation;
		}

		internal void AddChanges(HashSet<uint>? appIDs = null, HashSet<uint>? packageIDs = null, HashSet<uint>? newOwnedPackageIDs = null, bool ignoreFailedApps = false) {
			if (appIDs != null) {
				ChangedApps.UnionWith(appIDs);

				if (ignoreFailedApps) {
					ChangedApps.ExceptWith(IgnoredApps);
				}
			}

			if (packageIDs != null) {
				ChangedPackages.UnionWith(packageIDs);
			}

			if (newOwnedPackageIDs != null) {
				NewOwnedPackages.UnionWith(newOwnedPackageIDs);
			}

			Utilities.InBackground(Save);
		}

		internal void RemoveChange(uint? appID = null, uint? packageID = null, uint? newOwnedPackageID = null) {
			if (appID != null) {
				ChangedApps.Remove(appID.Value);
			}

			if (packageID != null) {
				ChangedPackages.Remove(packageID.Value);
			}

			if (newOwnedPackageID != null) {
				NewOwnedPackages.Remove(newOwnedPackageID.Value);
			}
		}

		internal void SaveChanges() {
			Utilities.InBackground(Save);
		}

		internal void ClearQueue() {
			Packages.RemoveWhere(package => ActivationQueue.ActivationTypes.Contains(package.Type));
			ChangedApps.Clear();
			ChangedPackages.Clear();
			Utilities.InBackground(Save);
		}

		internal void CancelRemoval() {
			Packages.RemoveWhere(package => RemovalQueue.RemovalTypes.Contains(package.Type));
			Utilities.InBackground(Save);
		}

		internal void AddWaitlistedPlaytest(uint appID) {
			WaitlistedPlaytests.Add(appID);
			
			Utilities.InBackground(Save);
		}

		internal void UpdateSeenPackages(List<SteamApps.LicenseListCallback.License> newLicenses) {
			SeenPackages.UnionWith(newLicenses.Select(license => license.PackageID));

			// Keep track of how many free licenses we activated to enforce the free packages limit
			// This is to catch packages that were activated, but didn't return a success status, or were activated outside of the plugin
			/* NOTE: The below code will not capture all recent activations.  If Steam removes a demo from your account, but you add it back, 
			 then the package will re-appear with the original TimeCreated value. Activations like these are instead logged when steam reports a
			 successful activation.
			 */
			// TODO: Do other PaymentMethod values also count against the free package limit?
			foreach(SteamApps.LicenseListCallback.License license in newLicenses) {
				if (license.PaymentMethod == EPaymentMethod.Complimentary &&
					license.TimeCreated.ToLocalTime() > DateTime.Now.AddMinutes(-1 * ActivationQueue.ActivationPeriodMinutes)
				) {
					AddActivation(license.TimeCreated.ToLocalTime(), packageIDs: [ license.PackageID ]);
				}
			}

			Utilities.InBackground(Save);
		}

		internal void IgnoreApp(uint appID) {
			IgnoredApps.Add(appID);

			Utilities.InBackground(Save);
		}
	}
}

================================================
FILE: FreePackages/Data/Cache/GlobalCache.cs
================================================
using System;
using System.IO;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Helpers;
using ArchiSteamFarm.Helpers.Json;

namespace FreePackages {
	internal sealed class GlobalCache : SerializableFile {
		private static string SharedFilePath => Path.Combine(ArchiSteamFarm.SharedInfo.ConfigDirectory, $"{nameof(FreePackages)}.cache");

		[JsonInclude]
		internal uint LastChangeNumber { get; private set; }

		[JsonInclude]
		internal uint LastASFInfoItemCount { get; private set; }
		
		public bool ShouldSerializeLastChangeNumber() => LastChangeNumber > 0;
		public bool ShouldSerializeLastASFInfoItemCount() => LastASFInfoItemCount > 0;

		[JsonConstructor]
		internal GlobalCache() {
			FilePath = SharedFilePath;
		}

		protected override Task Save() => Save(this);

		internal static async Task<GlobalCache> CreateOrLoad() {
			if (!File.Exists(SharedFilePath)) {
				return new GlobalCache();
			}

			GlobalCache? globalCache;
			try {
				string json = await File.ReadAllTextAsync(SharedFilePath).ConfigureAwait(false);

				if (string.IsNullOrEmpty(json)) {
					ASF.ArchiLogger.LogGenericError(string.Format(ArchiSteamFarm.Localization.Strings.ErrorIsEmpty, nameof(json)));

					return new GlobalCache();
				}

				globalCache = json.ToJsonObject<GlobalCache>();
			} catch (Exception e) {
				ASF.ArchiLogger.LogGenericException(e);

				return new GlobalCache();
			}

			if (globalCache == null) {
				ASF.ArchiLogger.LogNullError(globalCache);

				return new GlobalCache();
			}
			
			return globalCache;
		}

		internal void UpdateChangeNumber(uint currentChangeNumber) {
			LastChangeNumber = currentChangeNumber;

			Utilities.InBackground(Save);
		}
		
		internal void UpdateASFInfoItemCount(uint currentASFInfoItemCount) {
			LastASFInfoItemCount = currentASFInfoItemCount;

			Utilities.InBackground(Save);
		}
	}
}

================================================
FILE: FreePackages/Data/External/ASFInfo.cs
================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Web.Responses;
using FreePackages.Localization;

// There are limitations to using PICS for discovery such that an account that's online 24/7 can still miss certain free games
// To fill in some of these gaps, we periodically check the free apps/subs list provided by https://github.com/C4illin/ASFinfo
// For more information see here: https://github.com/Citrinate/FreePackages/commit/7541807f10e8dde53b1352a2c103b867e5446fa1#commitcomment-137669223

namespace FreePackages {
	internal static class ASFInfo {
		private static Uri Source = new("https://gist.githubusercontent.com/C4illin/e8c5cf365d816f2640242bf01d8d3675/raw/Steam%2520Codes");
		private static readonly Regex SourceLine = new Regex("(?<type>[as])/(?<id>[0-9]+)", RegexOptions.CultureInvariant | RegexOptions.IgnoreCase); // Match examples: a/12345 or s/12345
		private static TimeSpan UpdateFrequency = TimeSpan.FromHours(1);

		private static Timer UpdateTimer = new(async e => await DoUpdate().ConfigureAwait(false), null, Timeout.Infinite, Timeout.Infinite);
		
		internal static void Update() {
			UpdateTimer.Change(TimeSpan.FromMinutes(15), UpdateFrequency);
		}

		private static async Task DoUpdate() {
			ArgumentNullException.ThrowIfNull(ASF.WebBrowser);
			ArgumentNullException.ThrowIfNull(FreePackages.GlobalCache);

			StreamResponse? response = await ASF.WebBrowser.UrlGetToStream(Source).ConfigureAwait(false);

			if (response == null) {
				ASF.ArchiLogger.LogNullError(response);

				return;
			}

			if (response.Content == null) {
				ASF.ArchiLogger.LogNullError(response.Content);

				return;
			}

			HashSet<uint> appIDs = new();
			HashSet<uint> packageIDs = new();
			uint itemCount = 0;

			try {
				using (StreamReader sr = new StreamReader(response.Content)) {
					while (sr.Peek() >= 0) {
						itemCount++;
						string? line = sr.ReadLine();

						if (line == null) {
							ASF.ArchiLogger.LogNullError(line);

							return;
						}

						if (itemCount <= FreePackages.GlobalCache.LastASFInfoItemCount) {
							continue;
						}

						Match item = SourceLine.Match(line);

						if (!item.Success) {
							ASF.ArchiLogger.LogGenericError(String.Format("{0}: {1}", Strings.ASFInfoParseFailed, line));

							return;
						}

						if (!uint.TryParse(item.Groups["id"].Value, out uint id)) {
							ASF.ArchiLogger.LogGenericError(String.Format("{0}: {1}", Strings.ASFInfoParseFailed, line));

							return;
						}

						if (item.Groups["type"].Value == "a") {
							// App
							appIDs.Add(id);
						} else if (item.Groups["type"].Value == "s") {
							// Sub
							packageIDs.Add(id);
						}
					}
				}
			} catch (Exception e) {
				ASF.ArchiLogger.LogGenericException(e);

				return;
			}
			
			if (appIDs.Count == 0 && packageIDs.Count == 0) {
				return;
			}

			PackageHandler.Handlers.Values.ToList().ForEach(x => x.BotCache.AddChanges(appIDs, packageIDs));
			FreePackages.GlobalCache.UpdateASFInfoItemCount(itemCount);
			Utilities.InBackground(async() => await PackageHandler.HandleChanges().ConfigureAwait(false));
		}
	}
}

================================================
FILE: FreePackages/Data/External/CardApps.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Web.Responses;
using FreePackages.Localization;

namespace FreePackages {
	internal static class CardApps {
		internal static HashSet<uint> AppIDs = new();
		
		private static Timer UpdateTimer = new(async e => await DoUpdate().ConfigureAwait(false), null, Timeout.Infinite, Timeout.Infinite);
		private static TimeSpan UpdateFrequency = TimeSpan.FromHours(1);

		internal static void Update() {
			UpdateTimer.Change(TimeSpan.Zero, UpdateFrequency);
		}

		private static async Task DoUpdate() {
			ArgumentNullException.ThrowIfNull(ASF.WebBrowser);

			Uri request = new("https://raw.githubusercontent.com/nolddor/steam-badges-db/main/data/badges.min.json");
			ObjectResponse<Badges>? response = await ASF.WebBrowser.UrlGetToJsonObject<Badges>(request).ConfigureAwait(false);

			if (response == null) {
				ASF.ArchiLogger.LogGenericDebug(Strings.BadgeDataFetchFailed);
				UpdateTimer.Change(TimeSpan.FromMinutes(1), UpdateFrequency);

				return;
			}

			try {
				ArgumentNullException.ThrowIfNull(response.Content);
				
				AppIDs = response.Content.Data.Keys.Select(uint.Parse).ToHashSet();
			} catch (Exception e) {
				ASF.ArchiLogger.LogGenericException(e);
				ASF.ArchiLogger.LogGenericError(Strings.BadgeDataParsingFailed);

				return;
			}
		}

		private sealed class Badges {
			[JsonExtensionData]
			[JsonInclude]
			internal Dictionary<string, JsonElement> Data { get; private init; } = new();
		}
	}
}

================================================
FILE: FreePackages/Data/PICS/ProductInfo.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Steam;
using SteamKit2;

namespace FreePackages {
	internal static class ProductInfo {
		private static SemaphoreSlim ProductInfoSemaphore = new SemaphoreSlim(1, 1);
		internal const int ProductInfoLimitingDelaySeconds = 10;
		internal const int ItemsPerProductInfoRequest = 255;

		internal async static Task<List<SteamApps.PICSProductInfoCallback>?> GetProductInfo(HashSet<uint>? appIDs = null, HashSet<uint>? packageIDs = null, CancellationToken? cancellationToken = null) {
			List<SteamApps.PICSProductInfoCallback> productInfo = new();

			foreach ((HashSet<uint>? batchedAppIDs, HashSet<uint>? batchedPackageIDs) in GetProductIDBatches(appIDs, packageIDs)) {
				cancellationToken?.ThrowIfCancellationRequested();

				List<SteamApps.PICSProductInfoCallback>? partialProductInfo = await FetchProductInfo(batchedAppIDs, batchedPackageIDs).ConfigureAwait(false);
				if (partialProductInfo == null) {
					return null;
				}

				productInfo = productInfo.Concat(partialProductInfo).ToList();
			}

			return productInfo;
		}

		internal static IEnumerable<(HashSet<uint>?, HashSet<uint>?)> GetProductIDBatches(HashSet<uint>? appIDs = null, HashSet<uint>? packageIDs = null) {
			if ((appIDs?.Count ?? 0) + (packageIDs?.Count ?? 0) <= ItemsPerProductInfoRequest) {
				 yield return (appIDs, packageIDs);
			} else {
				if (appIDs != null) {
					for (int i = 0; i < Math.Ceiling((decimal) appIDs.Count / ItemsPerProductInfoRequest); i++) {
						HashSet<uint> batchedAppIDs = appIDs.Skip(i * ItemsPerProductInfoRequest).Take(ItemsPerProductInfoRequest).ToHashSet<uint>();

						yield return (batchedAppIDs, null);
					}
				}

				if (packageIDs != null) {
					for (int i = 0; i < Math.Ceiling((decimal) packageIDs.Count / ItemsPerProductInfoRequest); i++) {
						HashSet<uint> batchedPackageIDs = packageIDs.Skip(i * ItemsPerProductInfoRequest).Take(ItemsPerProductInfoRequest).ToHashSet<uint>();

						yield return (null, batchedPackageIDs);
					}
				}
			}
		}

		private async static Task<List<SteamApps.PICSProductInfoCallback>?> FetchProductInfo(IEnumerable<uint>? appIDs = null, IEnumerable<uint>? packageIDs = null) {
			await ProductInfoSemaphore.WaitAsync().ConfigureAwait(false);
			try {
				Bot? bot = Bot.BotsReadOnly?.Values.FirstOrDefault(static bot => bot.IsConnectedAndLoggedOn);
				if (bot == null) {
					return null;
				}

				var apps = appIDs == null ? Enumerable.Empty<SteamApps.PICSRequest>() : appIDs.Select(x => new SteamApps.PICSRequest(x));
				var packages = packageIDs == null ? Enumerable.Empty<SteamApps.PICSRequest>() : packageIDs.Select(x => new SteamApps.PICSRequest(x, ASF.GlobalDatabase?.PackageAccessTokensReadOnly.GetValueOrDefault(x, (ulong) 0) ?? 0));
				var response = await bot.SteamApps.PICSGetProductInfo(apps, packages).ToLongRunningTask().ConfigureAwait(false);

				return response.Results?.ToList();
			} catch (Exception e) {
				ASF.ArchiLogger.LogGenericWarningException(e);

				return null;
			} finally {
				Utilities.InBackground(
					async() => {
						await Task.Delay(TimeSpan.FromSeconds(ProductInfoLimitingDelaySeconds)).ConfigureAwait(false);
						ProductInfoSemaphore.Release();
					}
				);
			}
		}
	}
}

================================================
FILE: FreePackages/FreePackages.cs
================================================
using System;
using System.Collections.Generic;
using System.Composition;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Steam;
using ArchiSteamFarm.Plugins.Interfaces;
using SteamKit2;
using System.Text.Json;
using ArchiSteamFarm.Helpers.Json;

namespace FreePackages {
	[Export(typeof(IPlugin))]
	public sealed class FreePackages : IASF, IBotModules, ISteamPICSChanges, IBotSteamClient, IBotConnection, IBotCommand2, IGitHubPluginUpdates {
		public string Name => nameof(FreePackages);
		public string RepositoryName => "Citrinate/FreePackages";
		public Version Version => typeof(FreePackages).Assembly.GetName().Version ?? new Version("0");
		internal static GlobalCache? GlobalCache;

		public Task OnLoaded() {
			ASF.ArchiLogger.LogGenericInfo("Free Packages ASF Plugin by Citrinate");

			return Task.CompletedTask;
		}

		public async Task<string?> OnBotCommand(Bot bot, EAccess access, string message, string[] args, ulong steamID = 0) {
			return await Commands.Response(bot, access, steamID, message, args).ConfigureAwait(false);
		}

		public async Task OnASFInit(IReadOnlyDictionary<string, JsonElement>? additionalConfigProperties = null) {
			if (GlobalCache == null) {
				GlobalCache = await GlobalCache.CreateOrLoad().ConfigureAwait(false);
			}

			CardApps.Update();
			ASFInfo.Update();
		}

		public async Task OnBotInitModules(Bot bot, IReadOnlyDictionary<string, JsonElement>? additionalConfigProperties = null) {
			if (additionalConfigProperties == null) {
				return;
			}

			bool isEnabled = false;
			uint? packageLimit = null;
			bool pauseWhilePlaying = false;
			List<FilterConfig> filterConfigs = new();

			foreach (KeyValuePair<string, JsonElement> configProperty in additionalConfigProperties) {
				switch (configProperty.Key) {
					case "EnableFreePackages" when (configProperty.Value.ValueKind == JsonValueKind.True || configProperty.Value.ValueKind == JsonValueKind.False): {
						isEnabled = configProperty.Value.GetBoolean();
						bot.ArchiLogger.LogGenericInfo("Enable Free Packages : " + isEnabled.ToString());
						break;
					}
					
					case "PauseFreePackagesWhilePlaying" when (configProperty.Value.ValueKind == JsonValueKind.True || configProperty.Value.ValueKind == JsonValueKind.False): {
						pauseWhilePlaying = configProperty.Value.GetBoolean();
						bot.ArchiLogger.LogGenericInfo("Pause Free Packages While Playing : " + pauseWhilePlaying.ToString());
						break;
					}

					case "FreePackagesPerHour" or "FreePackagesLimit" when configProperty.Value.ValueKind == JsonValueKind.Number: {
						packageLimit = configProperty.Value.ToJsonObject<uint>();
						bot.ArchiLogger.LogGenericInfo("Free Packages Limit : " + packageLimit.ToString());
						break;
					}

					case "FreePackagesFilter": {
						FilterConfig? filter = configProperty.Value.ToJsonObject<FilterConfig>();
						if (filter != null) {
							bot.ArchiLogger.LogGenericInfo("Free Packages Filter : " + filter.ToJsonText());
							filterConfigs.Add(filter);
						}
						break;
					}
					
					case "FreePackagesFilters": {
						List<FilterConfig>? filters = configProperty.Value.ToJsonObject<List<FilterConfig>>();
						if (filters != null) {
							bot.ArchiLogger.LogGenericInfo("Free Packages Filters : " + filters.ToJsonText());
							filterConfigs.AddRange(filters);
						}
						break;
					}
				}
			}
			
			if (isEnabled) {
				await PackageHandler.AddHandler(bot, filterConfigs, packageLimit, pauseWhilePlaying).ConfigureAwait(false);
			}
		}

		public Task<uint> GetPreferredChangeNumberToStartFrom() {
			return Task.FromResult(GlobalCache?.LastChangeNumber ?? 0);
		}

		public Task OnPICSChanges(uint currentChangeNumber, IReadOnlyDictionary<uint, SteamApps.PICSChangesCallback.PICSChangeData> appChanges, IReadOnlyDictionary<uint, SteamApps.PICSChangesCallback.PICSChangeData> packageChanges) {
			PICSHandler.OnPICSChanges(currentChangeNumber, appChanges, packageChanges);
			
			return Task.CompletedTask;
		}

		public async Task OnPICSChangesRestart(uint currentChangeNumber) {
			await PICSHandler.OnPICSRestart(currentChangeNumber).ConfigureAwait(false);
		}

		public Task OnBotSteamCallbacksInit(Bot bot, CallbackManager callbackManager) {
			callbackManager.Subscribe<SteamApps.LicenseListCallback>(callback => OnLicenseList(bot, callback));

			return Task.CompletedTask;
		}

		public Task<IReadOnlyCollection<ClientMsgHandler>?> OnBotSteamHandlersInit(Bot bot) {
			return Task.FromResult<IReadOnlyCollection<ClientMsgHandler>?>(new List<ClientMsgHandler> { SteamHandler.AddHandler(bot) });
		}

		private static void OnLicenseList (Bot bot, SteamApps.LicenseListCallback callback) {
			PackageHandler.OnLicenseList(bot, callback);
		}

		public async Task OnBotLoggedOn(Bot bot) {
			await PackageHandler.OnBotLoggedOn(bot).ConfigureAwait(false);
		}

		public Task OnBotDisconnected(Bot bot, EResult reason) {
			return Task.FromResult(0);
		}
	}
}


================================================
FILE: FreePackages/FreePackages.csproj
================================================
<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <Authors>Citrinate</Authors>
    <CoreCompileDependsOn>PrepareResources;$(CompileDependsOn)</CoreCompileDependsOn>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.ResxSourceGenerator" PrivateAssets="all" />
    <PackageReference Include="Microsoft.AspNetCore.OpenApi" IncludeAssets="compile" />
    <PackageReference Include="System.Composition.AttributedModel" />
  </ItemGroup>

  <ItemGroup>
    <ProjectReference Include="..\ArchiSteamFarm\ArchiSteamFarm\ArchiSteamFarm.csproj" />
  </ItemGroup>

  <ItemGroup>
      <InternalsVisibleTo Include="FreePackages.Tests" />
  </ItemGroup>

  <ItemGroup>
    <EmbeddedResource Update="Localization\Strings.resx" />
  </ItemGroup>

</Project>


================================================
FILE: FreePackages/Handlers/PICSHandler.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Steam;
using FreePackages.Localization;
using SteamKit2;

namespace FreePackages {
	internal static class PICSHandler {
		private static SemaphoreSlim PICSChangesSemaphore = new SemaphoreSlim(1, 1);
		private const int PICSChangesLimitingDelaySeconds = 10;

		internal static void OnPICSChanges(uint currentChangeNumber, IReadOnlyDictionary<uint, SteamApps.PICSChangesCallback.PICSChangeData> appChanges, IReadOnlyDictionary<uint, SteamApps.PICSChangesCallback.PICSChangeData> packageChanges) {
			if (FreePackages.GlobalCache == null) {
				throw new InvalidOperationException(nameof(GlobalCache));
			}

			if (currentChangeNumber <= FreePackages.GlobalCache.LastChangeNumber) {
				return;
			}

			PackageHandler.AddChanges(appChanges, packageChanges);
			FreePackages.GlobalCache.UpdateChangeNumber(currentChangeNumber);
			
			return;
		}

		internal async static Task OnPICSRestart(uint currentChangeNumber) {
			if (PackageHandler.Handlers.Count == 0) {
				return;	
			}

			if (FreePackages.GlobalCache == null) {
				throw new InvalidOperationException(nameof(GlobalCache));
			}

			uint oldChangeNumber = FreePackages.GlobalCache.LastChangeNumber;
			ASF.ArchiLogger.LogGenericDebug(String.Format(Strings.PICSRestart, oldChangeNumber, currentChangeNumber));

			// ASF restarts PICS if either apps or packages needs a full update.  Check the old change number, as one of them might still be good.
			SteamApps.PICSChangesCallback? picsChanges = await FetchPICSChanges(oldChangeNumber, sendAppChangeList: false, sendPackageChangeList: true).ConfigureAwait(false);
			if (picsChanges == null) {
				return;
			}

			if (!picsChanges.RequiresFullAppUpdate) {
				PackageHandler.AddChanges(picsChanges.AppChanges, new Dictionary<uint, SteamApps.PICSChangesCallback.PICSChangeData>());
			} else {
				ASF.ArchiLogger.LogGenericDebug(Strings.MissedApps);
				
				// Search for the oldest change number which is still valid for apps
				var appChanges = await FindOldestPICSChanges(oldChangeNumber + 1, picsChanges.CurrentChangeNumber, findApps: true);
				if (appChanges != null) {
					ASF.ArchiLogger.LogGenericDebug(String.Format(Strings.RecoveredApps, appChanges.AppChanges.Count, appChanges.LastChangeNumber + 1));

					PackageHandler.AddChanges(appChanges.AppChanges, new Dictionary<uint, SteamApps.PICSChangesCallback.PICSChangeData>());
				}
			}

			if (!picsChanges.RequiresFullPackageUpdate) {
				PackageHandler.AddChanges(new Dictionary<uint, SteamApps.PICSChangesCallback.PICSChangeData>(), picsChanges.PackageChanges);
			} else {
				ASF.ArchiLogger.LogGenericDebug(Strings.MissedPackages);

				// Search for the oldest change number which is still valid for packages
				var packageChanges = await FindOldestPICSChanges(oldChangeNumber + 1, picsChanges.CurrentChangeNumber, findApps: false);
				if (packageChanges != null) {
					ASF.ArchiLogger.LogGenericDebug(String.Format(Strings.RecoveredPackages, packageChanges.PackageChanges.Count, packageChanges.LastChangeNumber + 1));

					PackageHandler.AddChanges(new Dictionary<uint, SteamApps.PICSChangesCallback.PICSChangeData>(), packageChanges.PackageChanges);
				}
			}

			if (currentChangeNumber > FreePackages.GlobalCache.LastChangeNumber) {
				FreePackages.GlobalCache.UpdateChangeNumber(currentChangeNumber);
			}
		}

		private async static Task<SteamApps.PICSChangesCallback?> FindOldestPICSChanges(uint minValidChangeNumber, uint maxValidChangeNumber, bool findApps) {
			if (minValidChangeNumber >= maxValidChangeNumber) {
				return null;
			}

			bool sendAppChangeList = findApps;
			bool sendPackageChangeList = !findApps;
			uint changeNumber = maxValidChangeNumber - ((uint) Math.Floor((maxValidChangeNumber - minValidChangeNumber) / 2.0));
			SteamApps.PICSChangesCallback? oldestPicsChanges = null;
			
			do {
				SteamApps.PICSChangesCallback? picsChanges = await FetchPICSChanges(changeNumber, sendAppChangeList, sendPackageChangeList).ConfigureAwait(false);
				if (picsChanges == null) {
					break;
				}

				bool isValid = (findApps && !picsChanges.RequiresFullAppUpdate) || (!findApps && !picsChanges.RequiresFullPackageUpdate);
				if (isValid) {
					oldestPicsChanges = picsChanges;
					maxValidChangeNumber = changeNumber;
				} else {
					minValidChangeNumber = changeNumber;
				}

				changeNumber = maxValidChangeNumber - Math.Max(1, ((uint) Math.Floor((maxValidChangeNumber - minValidChangeNumber) / 2.0)));
			} while (changeNumber > minValidChangeNumber);

			return oldestPicsChanges;
		}

		private async static Task<SteamApps.PICSChangesCallback?> FetchPICSChanges(uint changeNumber, bool sendAppChangeList = true, bool sendPackageChangeList = true) {
			await PICSChangesSemaphore.WaitAsync().ConfigureAwait(false);
			try {
				Bot? refreshBot = GetRefreshBot();
				if (refreshBot == null) {
					return null;
				}

				return await refreshBot.SteamApps.PICSGetChangesSince(changeNumber, sendAppChangeList, sendPackageChangeList).ToLongRunningTask().ConfigureAwait(false);
			} catch (Exception e) {
				ASF.ArchiLogger.LogGenericWarningException(e);

				return null;
			} finally {
				Utilities.InBackground(
					async() => {
						await Task.Delay(TimeSpan.FromSeconds(PICSChangesLimitingDelaySeconds)).ConfigureAwait(false);
						PICSChangesSemaphore.Release();
					}
				);
			}
		}

		private static Bot? GetRefreshBot() => Bot.BotsReadOnly?.Values.FirstOrDefault(static bot => bot.IsConnectedAndLoggedOn);
	}
}

================================================
FILE: FreePackages/Handlers/PackageHandler.cs
================================================
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Collections;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Steam;
using FreePackages.Localization;
using SteamKit2;

namespace FreePackages {
	internal sealed class PackageHandler : IDisposable {
		internal readonly Bot Bot;
		internal readonly BotCache BotCache;
		internal readonly PackageFilter PackageFilter;
		private readonly ActivationQueue ActivationQueue;
		private readonly RemovalQueue RemovalQueue;
		private CancellationTokenSource? RemovalCancellation;
		ConcurrentHashSet<Package> PackagesToRemove = new(new PackageComparer());
		internal static ConcurrentDictionary<string, PackageHandler> Handlers = new();

		private readonly Timer UserDataRefreshTimer;
		private static SemaphoreSlim AddHandlerSemaphore = new SemaphoreSlim(1, 1);
		private static SemaphoreSlim ProcessChangesSemaphore = new SemaphoreSlim(1, 1);

		private PackageHandler(Bot bot, BotCache botCache, List<FilterConfig> filterConfigs, uint? packageLimit, bool pauseWhilePlaying) {
			Bot = bot;
			BotCache = botCache;
			PackageFilter = new PackageFilter(botCache, filterConfigs);
			ActivationQueue = new ActivationQueue(bot, botCache, pauseWhilePlaying, packageLimit, PackageFilter);
			RemovalQueue = new RemovalQueue(bot, botCache, pauseWhilePlaying);
			UserDataRefreshTimer = new Timer(async e => await FetchUserData().ConfigureAwait(false), null, Timeout.Infinite, Timeout.Infinite);
		}

		public void Dispose() {
			ActivationQueue.Dispose();
			UserDataRefreshTimer.Dispose();
		}

		internal static async Task AddHandler(Bot bot, List<FilterConfig> filterConfigs, uint? packageLimit, bool pauseWhilePlaying) {
			if (Handlers.ContainsKey(bot.BotName)) {
				Handlers[bot.BotName].Dispose();
				Handlers.TryRemove(bot.BotName, out PackageHandler? _);
			}

			await AddHandlerSemaphore.WaitAsync().ConfigureAwait(false);
			try {
				if (filterConfigs.Any(filterConfig => filterConfig.PlaytestMode != EPlaytestMode.None)) {
					// Only allow 1 bot to request playtests
					int numBotsThatIncludePlaytests = Handlers.Values.Where(x => x.PackageFilter.FilterConfigs.Any(filterConfig => filterConfig.PlaytestMode != EPlaytestMode.None)).Count();
					if (numBotsThatIncludePlaytests > 0) {
						filterConfigs.ForEach(filterConfig => filterConfig.PlaytestMode = EPlaytestMode.None);
						bot.ArchiLogger.LogGenericInfo(Strings.PlaytestConfigLimitTriggered);
					}
				}

				string cacheFilePath = Bot.GetFilePath(String.Format("{0}_{1}", bot.BotName, nameof(FreePackages)), Bot.EFileType.Database);
				BotCache? botCache = await BotCache.CreateOrLoad(cacheFilePath).ConfigureAwait(false);
				if (botCache == null) {
					bot.ArchiLogger.LogGenericError(String.Format(ArchiSteamFarm.Localization.Strings.ErrorDatabaseInvalid, cacheFilePath));
					botCache = new(cacheFilePath);
				}

				Handlers.TryAdd(bot.BotName, new PackageHandler(bot, botCache, filterConfigs, packageLimit, pauseWhilePlaying));
			} finally {
				AddHandlerSemaphore.Release();
			}
		}

		internal static void OnLicenseList(Bot bot, SteamApps.LicenseListCallback callback) {
			if (!Handlers.ContainsKey(bot.BotName)) {
				return;
			}

			Handlers[bot.BotName].HandleLicenseList(callback);
		}

		internal static async Task OnBotLoggedOn(Bot bot) {
			if (!Handlers.ContainsKey(bot.BotName)) {
				return;
			}

			await Handlers[bot.BotName].FetchUserData().ConfigureAwait(false);
			Handlers[bot.BotName].ActivationQueue.Start();
			Handlers[bot.BotName].RemovalQueue.Start();
		}

		private void UpdateUserData() {
			UserDataRefreshTimer.Change(TimeSpan.Zero, TimeSpan.FromMinutes(15));
		}

		private async Task FetchUserData() {
			if (!Bot.IsConnectedAndLoggedOn) {
				UserDataRefreshTimer.Change(TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(15));

				return;
			}

			Steam.UserData? userData = await WebRequest.GetUserData(Bot).ConfigureAwait(false);
			if (userData == null) {
				UserDataRefreshTimer.Change(TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(15));
				Bot.ArchiLogger.LogGenericError(String.Format(ArchiSteamFarm.Localization.Strings.ErrorObjectIsNull, nameof(userData)));

				return;
			}

			Steam.UserInfo? userInfo = await WebRequest.GetUserInfo(Bot).ConfigureAwait(false);
			if (userInfo == null) {
				UserDataRefreshTimer.Change(TimeSpan.FromMinutes(1), TimeSpan.FromMinutes(15));
				Bot.ArchiLogger.LogGenericError(String.Format(ArchiSteamFarm.Localization.Strings.ErrorObjectIsNull, nameof(userInfo)));

				return;
			}

			PackageFilter.UpdateUserDetails(userData, userInfo);

			UserDataRefreshTimer.Change(TimeSpan.FromMinutes(15), TimeSpan.FromMinutes(15));
		}

		internal static void AddChanges(IReadOnlyDictionary<uint, SteamApps.PICSChangesCallback.PICSChangeData> appChanges, IReadOnlyDictionary<uint, SteamApps.PICSChangesCallback.PICSChangeData> packageChanges) {
			if (Handlers.Count == 0) {
				return;
			}

			// It's possible for a PICS change to effect thousands of apps and packages, Ex: https://steamdb.info/changelist/20445399/ (47,074 apps total, 31,529 packages total)
			// Store a list of changed apps/packages so that we can guarantee they'll all be processed eventually
			// Each bot has its own list, so that if any bots are offline, they'll be able to get caught up
			HashSet<uint> appIDs = appChanges.Select(x => x.Key).ToHashSet<uint>();
			HashSet<uint> packageIDs = packageChanges.Select(x => x.Key).ToHashSet<uint>();
			Handlers.Values.ToList().ForEach(x => x.BotCache.AddChanges(appIDs, packageIDs, ignoreFailedApps: true));

			Utilities.InBackground(async() => await HandleChanges().ConfigureAwait(false));
		}

		private async static Task<bool> IsReady(uint maxWaitTimeSeconds = 120) {
			DateTime timeoutTime = DateTime.Now.AddSeconds(maxWaitTimeSeconds);
			do {
				bool ready = Handlers.Values.Where(x => x.Bot.BotConfig.Enabled && !x.PackageFilter.Ready).Count() == 0;
				if (ready) {
					return true;
				}

				await Task.Delay(TimeSpan.FromSeconds(1)).ConfigureAwait(false);
			} while (maxWaitTimeSeconds == 0 || DateTime.Now < timeoutTime);

			return false;
		}

		internal async static Task HandleChanges() {
			if (!await ProcessChangesSemaphore.WaitAsync(0).ConfigureAwait(false)) {
				return;
			}

			try {
				await IsReady().ConfigureAwait(false);

				HashSet<uint> appIDs = Handlers.Values.Where(x => x.Bot.IsConnectedAndLoggedOn).SelectMany(x => x.BotCache.ChangedApps).ToHashSet<uint>();
				HashSet<uint> packageIDs = Handlers.Values.Where(x => x.Bot.IsConnectedAndLoggedOn).SelectMany(x => x.BotCache.ChangedPackages).ToHashSet<uint>();
				HashSet<uint> newOwnedPackageIDs = Handlers.Values.Where(x => x.Bot.IsConnectedAndLoggedOn).SelectMany(x => x.BotCache.NewOwnedPackages).ToHashSet<uint>();
				packageIDs.UnionWith(newOwnedPackageIDs);

				if (appIDs.Count == 0 && packageIDs.Count == 0) {
					return;
				}

				foreach ((HashSet<uint>? batchedAppIDs, HashSet<uint>? batchedPackageIDs) in ProductInfo.GetProductIDBatches(appIDs, packageIDs)) {
					var productInfo = await ProductInfo.GetProductInfo(batchedAppIDs, batchedPackageIDs).ConfigureAwait(false);
					if (productInfo == null) {
						continue;
					}

					await HandleProductInfo(productInfo).ConfigureAwait(false);
				}
			} finally {
				ProcessChangesSemaphore.Release();
			}
		}

		private async static Task HandleProductInfo(List<SteamApps.PICSProductInfoCallback> productInfos) {
			{ // Add wanted apps to the queue
				List<FilterableApp>? apps = await FilterableApp.GetFilterables(
					productInfos,
					app => {
						Handlers.Values.ToList().ForEach(x => x.BotCache.RemoveChange(appID: app.ID));

						return true;
					}
				).ConfigureAwait(false);

				if (apps == null) {
					ASF.ArchiLogger.LogGenericError(Strings.ProductInfoFetchFailed);

					return;
				}

				if (apps.Count > 0) {
					apps.ForEach(app => {
						if (app.Type == EAppType.Beta) {
							Handlers.Values.ToList().ForEach(x => x.HandlePlaytest(app));
						} else {
							Handlers.Values.ToList().ForEach(x => x.HandleFreeApp(app));
						}
					});
				}
			}

			{ // Add wanted packages to the queue or check new packages for free DLC
				HashSet<uint> newOwnedPackageIDs = Handlers.Values.Where(x => x.Bot.IsConnectedAndLoggedOn).SelectMany(x => x.BotCache.NewOwnedPackages).ToHashSet<uint>();
				List<FilterablePackage>? packages = await FilterablePackage.GetFilterables(
					productInfos,
					package => {
						Handlers.Values.ToList().ForEach(x => x.BotCache.RemoveChange(packageID: package.ID));

						return !newOwnedPackageIDs.Contains(package.ID);
					}
				).ConfigureAwait(false);

				if (packages == null) {
					ASF.ArchiLogger.LogGenericError(Strings.ProductInfoFetchFailed);

					return;
				}

				if (packages.Count > 0) {
					packages.ForEach(package => {
						if (newOwnedPackageIDs.Contains(package.ID)) {
							Handlers.Values.ToList().ForEach(x => x.HandleNewPackage(package));
						} else {
							Handlers.Values.ToList().ForEach(x => x.HandleFreePackage(package));
						}
					});
				}
			}

			// Remove invalid apps from the app change list
			foreach (uint unknownAppID in productInfos.SelectMany(static result => result.UnknownApps)) {
				Handlers.Values.ToList().ForEach(x => x.BotCache.RemoveChange(appID: unknownAppID));
			}

			// Remove invalid packages from the package change list
			foreach (uint unknownPackageID in productInfos.SelectMany(static result => result.UnknownPackages)) {
				Handlers.Values.ToList().ForEach(x => x.BotCache.RemoveChange(packageID: unknownPackageID));
				Handlers.Values.ToList().ForEach(x => x.BotCache.RemoveChange(newOwnedPackageID: unknownPackageID));
			}

			// Save changes to the app/package change lists
			Handlers.Values.ToList().ForEach(x => x.BotCache.SaveChanges());
		}

		private void HandleFreeApp(FilterableApp app) {
			if (!BotCache.ChangedApps.Contains(app.ID)) {
				return;
			}

			if (!PackageFilter.Ready) {
				return;
			}

			try {
				if (!PackageFilter.IsRedeemableApp(app)) {
					return;
				}

				if (!PackageFilter.IsWantedApp(app)) {
					return;
				}

				BotCache.AddPackage(new Package(EPackageType.App, app.ID, filterHash: PackageFilter.Hash));
			} finally {
				BotCache.RemoveChange(appID: app.ID);
			}
		}

		private void HandleFreePackage(FilterablePackage package) {
			if (!BotCache.ChangedPackages.Contains(package.ID)) {
				return;
			}

			if (!PackageFilter.Ready) {
				return;
			}

			try {
				if (!PackageFilter.IsRedeemablePackage(package)) {
					return;
				}

				if (!PackageFilter.IsWantedPackage(package)) {
					return;
				}

				if (BotCache.AddPackage(new Package(EPackageType.Sub, package.ID, package.StartTime, filterHash: PackageFilter.Hash))) {
					// Remove duplicates.  
					// Whenever we're trying to activate an app and also an package for that app, get rid of the app.
					// This is because the error messages for activating packages are more descriptive and useful.
					BotCache.RemoveAppPackages(package.PackageContentIDs);
				}
			} finally {
				BotCache.RemoveChange(packageID: package.ID);
			}
		}

		private void HandlePlaytest(FilterableApp app) {
			if (!BotCache.ChangedApps.Contains(app.ID)) {
				return;
			}

			if (!PackageFilter.Ready) {
				return;
			}

			try {
				if (app.Parent == null) {
					return;
				}

				if (!PackageFilter.IsRedeemablePlaytest(app)) {
					return;
				}

				if (!PackageFilter.IsWantedPlaytest(app)) {
					return;
				}

				BotCache.AddPackage(new Package(EPackageType.Playtest, app.Parent.ID, filterHash: PackageFilter.Hash));
			} finally {
				BotCache.RemoveChange(appID: app.ID);
			}
		}

		private void HandleNewPackage(FilterablePackage package) {
			if (!BotCache.NewOwnedPackages.Contains(package.ID)) {
				return;
			}

			try {
				if (package.PackageContents.Count == 0) {
					return;
				}

				// Check for free DLC on newly added packages
				HashSet<uint> dlcAppIDs = new();

				foreach (FilterableApp app in package.PackageContents) {
					if (String.IsNullOrEmpty(app.ListOfDLC)) {
						continue;
					}

					foreach (string dlcAppIDString in app.ListOfDLC.Split(",", StringSplitOptions.RemoveEmptyEntries)) {
						if (!uint.TryParse(dlcAppIDString, out uint dlcAppID) || (dlcAppID == 0)) {
							continue;
						}

						dlcAppIDs.Add(dlcAppID);
					}
				}

				if (dlcAppIDs.Count != 0) {
					BotCache.AddChanges(appIDs: dlcAppIDs);
					Utilities.InBackground(async() => await HandleChanges().ConfigureAwait(false));
				}
			} finally {
				BotCache.RemoveChange(newOwnedPackageID: package.ID);
			}
		}

		internal void HandleLicenseList(SteamApps.LicenseListCallback callback) {
			List<SteamApps.LicenseListCallback.License> newLicenses = callback.LicenseList.Where(license => !BotCache.SeenPackages.Contains(license.PackageID)).ToList();

			if (newLicenses.Count == 0) {
				return;
			}

			UpdateUserData();

			// Initialize SeenPackages
			if (BotCache.SeenPackages.Count == 0) {
				BotCache.UpdateSeenPackages(newLicenses);

				return;
			}

			BotCache.AddChanges(newOwnedPackageIDs: newLicenses.Select(license => license.PackageID).ToHashSet());
			BotCache.UpdateSeenPackages(newLicenses);
			Utilities.InBackground(async() => await HandleChanges().ConfigureAwait(false));
		}

		internal string GetStatus() {
			HashSet<string> responses = new HashSet<string>();

			// x packages queued. y activations used
			int activationsPastPeriod = Math.Min(BotCache.NumActivationsPastPeriod(), (int)ActivationQueue.MaxActivationsPerPeriod);
			responses.Add(String.Format(Strings.QueueStatus, ActivationQueue.ActivationsRemaining, activationsPastPeriod, ActivationQueue.ActivationsPerPeriod));

			// activations are paused
			if (ActivationQueue.PauseWhilePlaying && !Bot.IsPlayingPossible) {
				responses.Add(Strings.QueuePausedWhileIngame);
			}

			// activations will resume when
			if (activationsPastPeriod >= ActivationQueue.ActivationsPerPeriod) {
				DateTime resumeTime = BotCache.GetLastActivation()!.Value.AddMinutes(ActivationQueue.ActivationPeriodMinutes + 1);
				responses.Add(String.Format(Strings.QueueLimitedUntil, String.Format("{0:T}", resumeTime)));
			}

			// x apps and y packages discovered but not processed
			if (BotCache.ChangedApps.Count > 0 || BotCache.ChangedPackages.Count > 0) {
				responses.Add(String.Format(Strings.QueueDiscoveryStatus, BotCache.ChangedApps.Count, BotCache.ChangedPackages.Count));
			}

			// removing x packages
			if (RemovalQueue.RemovalsRemaining > 0) {
				responses.Add(String.Format(Strings.RemovingPackages, RemovalQueue.RemovalsRemaining));
			}

			return String.Join(" ", responses);
		}

		internal string ClearQueue() {
			int numPackages = BotCache.Packages.Where(package => ActivationQueue.ActivationTypes.Contains(package.Type)).Count();
			int numChangedApps = BotCache.ChangedApps.Count;
			int numChangedPackages = BotCache.ChangedPackages.Count;

			if (numPackages == 0 && numChangedApps == 0 && numChangedPackages == 0) {
				return Strings.QueueEmpty;
			}

			BotCache.ClearQueue();

			List<string> responses = new List<string>();

			if (numPackages > 0) {
				responses.Add(String.Format(Strings.PackagesRemoved, numPackages));
			}
			if (numChangedApps > 0) {
				responses.Add(String.Format(Strings.DiscoveredAppsRemoved, numChangedApps));
			}
			if (numChangedPackages > 0) {
				responses.Add(String.Format(Strings.DiscoveredPackagesRemoved, numChangedPackages));
			}

			return String.Join(" ", responses);
		}

		internal string AddPackage(EPackageType type, uint id, bool useFilter) {
			if (useFilter) {
				if (type == EPackageType.App) {
					BotCache.AddChanges(appIDs: new HashSet<uint> { id });

					return String.Format(Strings.DiscoveredAppsAdded, String.Format("app/{0}", id));
				} else {
					BotCache.AddChanges(packageIDs: new HashSet<uint> { id });

					return String.Format(Strings.DiscoveredPackagesAdded, String.Format("sub/{0}", id));
				}
			}

			BotCache.AddPackage(new Package(type, id));

			if (type == EPackageType.App) {
				return String.Format(Strings.AppsQueued, String.Format("app/{0}", id));
			} else {
				return String.Format(Strings.PackagesQueued, String.Format("sub/{0}", id));
			}
		}

		internal void AddPackages(HashSet<uint>? appIDs, HashSet<uint>? packageIDs, bool useFilter) {
			if (useFilter) {
				BotCache.AddChanges(appIDs, packageIDs);

				return;
			}

			HashSet<Package> packages = new();
			if (appIDs != null) {
				packages.UnionWith(appIDs.Select(static id => new Package(EPackageType.App, id)));
			}
			if (packageIDs != null) {
				packages.UnionWith(packageIDs.Select(static id => new Package(EPackageType.Sub, id)));
			}

			BotCache.AddPackages(packages);
		}

		internal async Task ScanRemovables(Dictionary<uint, string> removeablePackages, bool excludePlayed, bool removeAll, StatusReporter statusReporter) {
			if (RemovalCancellation != null) {
				statusReporter.Report(Bot, Strings.RemovalScanAlreadyRunning);

				return;
			}
			
			RemovalCancellation = new CancellationTokenSource();
			try {
				await ProcessChangesSemaphore.WaitAsync(RemovalCancellation.Token).ConfigureAwait(false);
				try {
					await IsReady().ConfigureAwait(false);

					Dictionary<uint, SteamKit2.Internal.CPlayer_GetOwnedGames_Response.Game>? ownedGameDetails = null;
					if (excludePlayed) {
						ownedGameDetails = await SteamHandler.Handlers[Bot.BotName].GetOwnedGames(Bot.SteamID).ConfigureAwait(false);
						if (ownedGameDetails == null) {
							statusReporter.Report(Bot, Strings.PlaytimeFetchFailed);

							return;
						}
					}

					var productInfos = await ProductInfo.GetProductInfo(packageIDs: removeablePackages.Keys.ToHashSet(), cancellationToken: RemovalCancellation.Token).ConfigureAwait(false);
					if (productInfos == null) {
						statusReporter.Report(Bot, Strings.ProductInfoFetchFailed);

						return;
					}

					List<FilterablePackage>? packages = await FilterablePackage.GetFilterables(productInfos, cancellationToken: RemovalCancellation.Token, onNonFreePackage: x => !removeAll).ConfigureAwait(false);
					if (packages == null) {
						statusReporter.Report(Bot, Strings.ProductInfoFetchFailed);

						return;
					}

					if (packages.Count == 0) {
						statusReporter.Report(Bot, Strings.RemovingNoPackages);

						return;
					}

					RemovalCancellation.Token.ThrowIfCancellationRequested();

					PackagesToRemove.Clear();
					List<string> previewResponses = [];
					var ownedPackageIDs = Bot.OwnedPackages.Keys.ToHashSet();			
					foreach (FilterablePackage package in packages) {
						if (!removeAll) {
							if (!PackageFilter.IsRedeemablePackage(package, ignoreAlreadyOwned: true)) {
								continue;
							}

							if (PackageFilter.IsWantedPackage(package, ignoreAgeFilters: true)) {
								continue;
							}
						}

						if (excludePlayed) {
							if (package.PackageContents.Any(app => {
								return (ownedGameDetails!.ContainsKey(app.ID) && ownedGameDetails![app.ID].playtime_forever > 0)
									|| (app.Type != EAppType.Demo && app.ParentID != null && ownedGameDetails!.ContainsKey(app.ParentID.Value) && ownedGameDetails![app.ParentID.Value].playtime_forever > 0);
							})) {
								continue;
							}
						}

						// Attempt to remove the app directly, which uses an API with a more generous rate limit
						if (package.PackageContents.Count == 1) {
							FilterableApp app = package.PackageContents.First();

							// Apparently the API for removing apps doesn't care which package is removed? (Haven't tested this) https://github.com/JustArchiNET/ArchiSteamFarm/issues/3434#issuecomment-2954303590
							// Only remove by app if this is the only package that can be removed
							int ownedPackagesWithApp = ASF.GlobalDatabase!.PackagesDataReadOnly.Where(x => ownedPackageIDs.Contains(x.Key) && x.Value.AppIDs != null && x.Value.AppIDs.Contains(app.ID)).Count();
							if (ownedPackagesWithApp <= 1) {
								PackagesToRemove.Add(new Package(EPackageType.RemoveApp, app.ID));
								previewResponses.Add(String.Format("app/{0} ({1})", app.ID, removeablePackages[package.ID]));

								continue;
							}
						}
						
						PackagesToRemove.Add(new Package(EPackageType.RemoveSub, package.ID));
						previewResponses.Add(String.Format("sub/{0} ({1})", package.ID, removeablePackages[package.ID]));
					}

					if (PackagesToRemove.Count == 0) {
						statusReporter.Report(Bot, Strings.RemovingNoUnwatedPackages);

						return;
					}

					statusReporter.Report(Bot, String.Format(Strings.RemovablePackagesFound, new object?[] {
						PackagesToRemove.Count, 
						String.Join(PackagesToRemove.Count > 100 ? ", " : Environment.NewLine, previewResponses),
						String.Format("!cancelremove {0}", Bot.BotName),
						String.Format("!confirmremove {0}", Bot.BotName),
						String.Format("!dontremove {0} <Licenses>", Bot.BotName)
					}));
				} finally {
					ProcessChangesSemaphore.Release();
				}
			} catch (OperationCanceledException) {
				statusReporter.Report(Bot, Strings.RemovalScanCancelled);
			} finally {
				RemovalCancellation?.Dispose();
				RemovalCancellation = null;
			}
		}

		internal string ConfirmRemoval() {
			if (PackagesToRemove.Count == 0) {
				return String.Format(Strings.RemovalScanNeeded, String.Format("!removefreepackages {0}", Bot.BotName));
			}

			int numRemovalsDiscovered = PackagesToRemove.Count;
			BotCache.AddPackages(PackagesToRemove);
			PackagesToRemove.Clear();

			return String.Format(Strings.RemovingPackages, numRemovalsDiscovered);
		}

		internal string ModifyRemovables(EPackageType type, uint id) {
			if (PackagesToRemove.Count == 0) {
				return String.Format(Strings.RemovalScanNeeded, String.Format("!removefreepackages {0}", Bot.BotName));
			}

			Package? package = PackagesToRemove.FirstOrDefault(package => package.Type == type && package.ID == id);
			if (package == null) {
				if (type == EPackageType.RemoveApp) {
					return String.Format(Strings.RemovalPackageNotFound, String.Format("app/{0}", id)) + " :steamthumbsdown:";
				} else {
					return String.Format(Strings.RemovalPackageNotFound, String.Format("sub/{0}", id)) + " :steamthumbsdown:";
				}
			}

			PackagesToRemove.Remove(package);

			if (type == EPackageType.RemoveApp) {
				return String.Format(Strings.RemovalPackageCancelled, String.Format("app/{0}", id));
			} else {
				return String.Format(Strings.RemovalPackageCancelled, String.Format("sub/{0}", id));
			}
		}

		internal string CancelRemoval() {
			bool stoppingScan = RemovalCancellation != null;
			int numRemovalsDiscovered = PackagesToRemove.Count;
			int numRemovals = BotCache.Packages.Where(package => RemovalQueue.RemovalTypes.Contains(package.Type)).Count();

			if (!stoppingScan && numRemovalsDiscovered == 0 && numRemovals == 0) {
				return Strings.RemovalQueueEmpty;
			}

			RemovalCancellation?.Cancel();
			PackagesToRemove.Clear();
			BotCache.CancelRemoval();

			List<string> responses = new List<string>();
			if (numRemovals > 0) {
				responses.Add(String.Format(Strings.RemovalsCancelled, numRemovals));
			}
			if (numRemovalsDiscovered > 0) {
				responses.Add(String.Format(Strings.RemovalScanCancelled));
			}
			if (stoppingScan) {
				responses.Add(String.Format(Strings.RemovalScanCanceling));
			}

			return String.Join(" ", responses);
		}
	}
}


================================================
FILE: FreePackages/Handlers/SteamHandler.cs
================================================
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Steam;
using SteamKit2;
using SteamKit2.Internal;

namespace FreePackages {
	internal sealed class SteamHandler : ClientMsgHandler {
		internal static ConcurrentDictionary<string, SteamHandler> Handlers = new();

		internal static SteamHandler AddHandler(Bot bot) {
			if (Handlers.ContainsKey(bot.BotName)) {
				Handlers.TryRemove(bot.BotName, out SteamHandler? _);
			}

			SteamHandler handler = new();
			Handlers.TryAdd(bot.BotName, handler);

			return handler;
		}

		public override void HandleMsg(IPacketMsg packetMsg) { }

		public async Task<Dictionary<uint, CPlayer_GetOwnedGames_Response.Game>?> GetOwnedGames(ulong steamID) {
			if ((steamID == 0) || !new SteamID(steamID).IsIndividualAccount) {
				throw new ArgumentOutOfRangeException(nameof(steamID));
			}

			if (Client == null) {
				throw new InvalidOperationException(nameof(Client));
			}

			if (!Client.IsConnected) {
				return null;
			}

			SteamUnifiedMessages steamUnifiedMessages = Client.GetHandler<SteamUnifiedMessages>() ?? throw new InvalidOperationException(nameof(SteamUnifiedMessages));
			Player unifiedPlayerService = steamUnifiedMessages.CreateService<Player>();

			CPlayer_GetOwnedGames_Request request = new() {
				steamid = steamID,
				include_appinfo = true,
				include_extended_appinfo = true,
				include_free_sub = true,
				include_played_free_games = true,
				skip_unvetted_apps = false
			};

			SteamUnifiedMessages.ServiceMethodResponse<CPlayer_GetOwnedGames_Response> response;

			try {
				response = await unifiedPlayerService.GetOwnedGames(request).ToLongRunningTask().ConfigureAwait(false);
			} catch (Exception e) {
				ASF.ArchiLogger.LogGenericWarningException(e);

				return null;
			}

			return response.Result == EResult.OK ? response.Body.games.ToDictionary(static game => (uint)game.appid, static game => game) : null;
		}
	}
}


================================================
FILE: FreePackages/Helpers/DeterministicHasher.cs
================================================
using System;
using System.Collections.Generic;
using System.Linq;

namespace FreePackages {
	internal static class DeterministicHasher {
		private const int FnvOffsetBias = unchecked((int) 2166136261);
		private const int FnvPrime = 16777619;

		internal static int Hash(int value) => Hash(FnvOffsetBias, value);
		internal static int Hash(uint value) => Hash(FnvOffsetBias, value);
		internal static int Hash(bool value) => Hash(FnvOffsetBias, value);
		internal static int Hash(string? str) => Hash(FnvOffsetBias, str);
		internal static int Hash(IEnumerable<string>? collection) => Hash(FnvOffsetBias, collection);
		internal static int Hash(IEnumerable<uint>? collection) => Hash(FnvOffsetBias, collection);
		internal static int Hash(IEnumerable<FilterConfig>? collection) => Hash(FnvOffsetBias, collection);

		internal static int Hash(int hash, int value) => unchecked((hash ^ value) * FnvPrime);
		internal static int Hash(int hash, uint value) => Hash(hash, (int) value);
		internal static int Hash(int hash, bool value) => Hash(hash, value ? 1 : 0);

		internal static int Hash(int hash, string? str) {
			if (str == null) {
				return hash;
			}

			foreach (char c in str) {
				hash = Hash(hash, c);
			}

			return hash;
		}

		internal static int Hash(int hash, IEnumerable<string>? collection) {
			if (collection == null) {
				return hash;
			}

			foreach (string item in collection.OrderBy(static x => x, StringComparer.Ordinal)) {
				hash = Hash(hash, item);
			}

			return hash;
		}

		internal static int Hash(int hash, IEnumerable<uint>? collection) {
			if (collection == null) {
				return hash;
			}

			foreach (uint item in collection.OrderBy(static x => x)) {
				hash = Hash(hash, item);
			}

			return hash;
		}

		internal static int Hash(int hash, IEnumerable<FilterConfig>? collection) {
			if (collection == null) {
				return hash;
			}

			foreach (FilterConfig item in collection.OrderBy(static x => x.GetHashCode())) {
				hash = Hash(hash, item.GetHashCode());
			}

			return hash;
		}
	}
}


================================================
FILE: FreePackages/Helpers/StatusReporter.cs
================================================
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.Steam;
using SteamKit2;

// For when long-running commands are issued through Steam chat, this is used to send status reports from the bot the command was sent to, to the user who issued the command
// If the commands weren't issued through Steam chat, this just logs the status reports

namespace FreePackages {
	internal sealed class StatusReporter {
		[JsonInclude]
		[JsonRequired]
		private ulong SenderSteamID; // When we send status reports, they'll come from this SteamID

		[JsonInclude]
		[JsonRequired]
		private ulong RecipientSteamID; // When we send status reports, they'll go to this SteamID

		private ConcurrentDictionary<Bot, List<string>> Reports = new();
		private ConcurrentDictionary<Bot, List<string>> PreviousReports = new();
		private uint ReportDelaySeconds;
		private uint ReportMaxDelaySeconds;
		private const uint DefaultReportDelaySeconds = 5;

		private Timer? ReportTimer;
		private DateTime? ReportMaxDelayTime = null;
		private SemaphoreSlim ReportSemaphore = new SemaphoreSlim(1, 1);

		internal StatusReporter(Bot? sender = null, ulong recipientSteamID = 0, uint reportDelaySeconds = DefaultReportDelaySeconds, uint? reportMaxDelaySeconds = null) {
			SenderSteamID = sender?.SteamID ?? 0;
			RecipientSteamID = recipientSteamID;
			ReportDelaySeconds = reportDelaySeconds;
			ReportMaxDelaySeconds = reportMaxDelaySeconds ?? reportDelaySeconds * 5;
		}

		[JsonConstructor]
		internal StatusReporter(ulong senderSteamID = 0, ulong recipientSteamID = 0) {
			SenderSteamID = senderSteamID;
			RecipientSteamID = recipientSteamID;
		}

		internal static StatusReporter StatusLogger() {
			// Create a status reporter that doesn't send messages through chat, it just logs everything
			return new StatusReporter(0, 0);
		}

		internal void Report(Bot reportingBot, string report, bool suppressDuplicateMessages = false, bool log = false) {
			if (log || SenderSteamID == 0 || RecipientSteamID == 0) {
				reportingBot.ArchiLogger.LogGenericInfo(report);
					
				return;
			}

			ReportSemaphore.Wait();
			try {
				if (suppressDuplicateMessages) {
					bool existsInReports = Reports.TryGetValue(reportingBot, out var reports) && reports.Contains(report);
					bool existsInPreviousReports = PreviousReports.TryGetValue(reportingBot, out var previousReports) && previousReports.Contains(report);

					if (existsInReports || existsInPreviousReports) {
						return;
					}
				}

				Reports.TryAdd(reportingBot, new List<string>());
				Reports[reportingBot].Add(report);

				// I prefer to send all reports in as few messages as possible
				// As long as reports continue to come in, we wait (until some limit, to avoid possibly waiting forever)

				double delayCorrectionSeconds = 0;
				if (ReportMaxDelayTime != null) {
					if (ReportMaxDelayTime <= DateTime.Now) {
						return;
					}

					delayCorrectionSeconds = Math.Max(0, (DateTime.Now.AddSeconds(ReportDelaySeconds) - ReportMaxDelayTime.Value).TotalSeconds);
				}

				if (ReportTimer != null) {
					ReportTimer.Change(Timeout.Infinite, Timeout.Infinite);
					ReportTimer.Dispose();
				}

				ReportTimer = new Timer(async _ => await Send().ConfigureAwait(false), null, TimeSpan.FromSeconds(ReportDelaySeconds - delayCorrectionSeconds), Timeout.InfiniteTimeSpan);

				if (ReportMaxDelayTime == null) {
					ReportMaxDelayTime = DateTime.Now.AddSeconds(ReportMaxDelaySeconds);
				}
			} finally {
				ReportSemaphore.Release();
			}
		}

		internal void ForceSend() {
			Utilities.InBackground(async() => await Send().ConfigureAwait(false));
		}

		private async Task Send() {
			await ReportSemaphore.WaitAsync().ConfigureAwait(false);
			try {
				ReportTimer?.Dispose();
				ReportMaxDelayTime = null;

				List<string> messages = new List<string>();
				List<Bot> bots = Reports.Keys.OrderBy(bot => bot.BotName).ToList();

				foreach (Bot bot in bots) {
					messages.Add(Commands.FormatBotResponse(bot, String.Join(Environment.NewLine, Reports[bot])));
					if (Reports[bot].Count > 1) {
						// Add an extra line if there's more than 1 message from a bot
						messages.Add("");
					}

					if (Reports.TryRemove(bot, out List<string>? previousReports)) {
						if (previousReports != null) {
							PreviousReports[bot] = previousReports;
						}
					}
				}

				if (messages.Count == 0) {
					return;
				}

				Bot? sender = SenderSteamID == 0 ? null : Bot.BotsReadOnly?.Values.FirstOrDefault(bot => bot.SteamID == SenderSteamID);
				if (sender == null 
					|| RecipientSteamID == 0 
					|| !new SteamID(RecipientSteamID).IsIndividualAccount 
					|| sender.SteamFriends.GetFriendRelationship(RecipientSteamID) != EFriendRelationship.Friend
				) {
					// Can't send a chat message through Steam, just log the report
					ASF.ArchiLogger.LogGenericInfo(String.Join(Environment.NewLine, messages));

					return;
				}
				
				try {
					if (!await sender.SendMessage(RecipientSteamID, String.Join(Environment.NewLine, messages)).ConfigureAwait(false)) {
						ASF.ArchiLogger.LogGenericInfo(String.Join(Environment.NewLine, messages));
					}
				} catch (Exception) {
					ASF.ArchiLogger.LogGenericInfo(String.Join(Environment.NewLine, messages));
				}
			} finally {
				ReportSemaphore.Release();
			}
		}
	}
}


================================================
FILE: FreePackages/IPC/Api/FreePackagesController.cs
================================================
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Threading.Tasks;
using ArchiSteamFarm.Core;
using ArchiSteamFarm.IPC.Controllers.Api;
using ArchiSteamFarm.IPC.Responses;
using ArchiSteamFarm.Steam;
using FreePackages.Localization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using SteamKit2;
using SteamKit2.Internal;

namespace FreePackages.IPC {
	[Route("Api/FreePackages")]
	public sealed class FreePackagesController : ArchiController {
		[HttpGet("{botNames:required}/GetChangesSince/{changeNumber:required}")]
		[EndpointSummary("Request changes for apps and packages since a given change number")]
		[ProducesResponseType(typeof(GenericResponse<SteamApps.PICSChangesCallback>), (int) HttpStatusCode.OK)]
		[ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.BadRequest)]
		public async Task<ActionResult<GenericResponse>> GetChangesSince(string botNames, uint changeNumber, bool showAppChanges = true, bool showPackageChanges = true) {
			if (string.IsNullOrEmpty(botNames)) {
				throw new ArgumentNullException(nameof(botNames));
			}

			HashSet<Bot>? bots = Bot.GetBots(botNames);
			if ((bots == null) || (bots.Count == 0)) {
				return BadRequest(new GenericResponse(false, string.Format(ArchiSteamFarm.Localization.Strings.BotNotFound, botNames)));
			}

			Bot? bot = bots.FirstOrDefault(static bot => bot.IsConnectedAndLoggedOn);
			if (bot == null) {
				return BadRequest(new GenericResponse(false, ArchiSteamFarm.Localization.Strings.BotNotConnected));
			}

			SteamApps.PICSChangesCallback picsChanges;
			try {
				picsChanges = await bot.SteamApps.PICSGetChangesSince(changeNumber, showAppChanges, showPackageChanges).ToLongRunningTask().ConfigureAwait(false);
			} catch (Exception e) {
				bot.ArchiLogger.LogGenericWarningException(e);

				return BadRequest(new GenericResponse(false, e.Message));
			}

			return Ok(new GenericResponse<SteamApps.PICSChangesCallback>(true, picsChanges));
		}

		[HttpGet("{botNames:required}/GetProductInfo")]
		[EndpointSummary("Request product information for a list of apps or packages")]
		[ProducesResponseType(typeof(GenericResponse<IEnumerable<SteamApps.PICSProductInfoCallback>>), (int) HttpStatusCode.OK)]
		[ProducesResponseType(typeof(byte[]), (int) HttpStatusCode.OK)]
		[ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.BadRequest)]
		public async Task<ActionResult<GenericResponse>> GetProductInfo(string botNames, string? appIDs, string? packageIDs, bool returnFirstRaw = false) {
			if (string.IsNullOrEmpty(botNames)) {
				throw new ArgumentNullException(nameof(botNames));
			}

			HashSet<Bot>? bots = Bot.GetBots(botNames);
			if ((bots == null) || (bots.Count == 0)) {
				return BadRequest(new GenericResponse(false, string.Format(ArchiSteamFarm.Localization.Strings.BotNotFound, botNames)));
			}

			Bot? bot = bots.FirstOrDefault(static bot => bot.IsConnectedAndLoggedOn);
			if (bot == null) {
				return BadRequest(new GenericResponse(false, ArchiSteamFarm.Localization.Strings.BotNotConnected));
			}

			IEnumerable<SteamApps.PICSProductInfoCallback> productInfos;
			try {
				List<SteamApps.PICSRequest> apps = appIDs == null ? new() : appIDs.Split(",").Select(x => new SteamApps.PICSRequest(uint.Parse(x))).ToList();
				List<SteamApps.PICSRequest> packages = packageIDs == null ? new() : packageIDs.Split(",").Select(x => new SteamApps.PICSRequest(uint.Parse(x), ASF.GlobalDatabase?.PackageAccessTokensReadOnly.GetValueOrDefault(uint.Parse(x), (ulong) 0) ?? 0)).ToList();
				var response = await bot.SteamApps.PICSGetProductInfo(apps, packages).ToLongRunningTask().ConfigureAwait(false);
				if (response.Results == null) {
					return BadRequest(new GenericResponse(false));
				}

				productInfos = response.Results;				
			} catch (Exception e) {
				bot.ArchiLogger.LogGenericWarningException(e);

				return BadRequest(new GenericResponse(false, e.Message));
			}

			if (returnFirstRaw) {
				var results = productInfos.SelectMany(static result => result.Apps.Values).Concat(productInfos.SelectMany(static result => result.Packages.Values));
				if (results.Count() == 0) {
					return File(Array.Empty<byte>(), "text/plain; charset=utf-8");
				}

				try {
					await using var kvMemory = new MemoryStream();
					results.First().KeyValues.SaveToStream(kvMemory, false);
					return File(kvMemory.ToArray(), "text/plain; charset=utf-8");
				} catch (Exception e) {
					bot.ArchiLogger.LogGenericWarningException(e);

					return BadRequest(new GenericResponse(false, e.Message));
				}
			}

			return Ok(new GenericResponse<IEnumerable<SteamApps.PICSProductInfoCallback>>(true, productInfos));
		}

		[HttpGet("{botName:required}/RequestFreeAppLicense")]
		[HttpPost("{botName:required}/RequestFreeAppLicense")]
		[EndpointSummary("Request a free license for given appids")]
		[ProducesResponseType(typeof(GenericResponse<SteamApps.FreeLicenseCallback>), (int) HttpStatusCode.OK)]
		[ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.BadRequest)]
		public async Task<ActionResult<GenericResponse>> RequestFreeAppLicense(string botName, [FromQuery] string appIDs) {
			if (string.IsNullOrEmpty(botName)) {
				throw new ArgumentNullException(nameof(botName));
			}

			Bot? bot = Bot.GetBot(botName);
			if (bot == null) {
				return BadRequest(new GenericResponse(false, string.Format(ArchiSteamFarm.Localization.Strings.BotNotFound, botName)));
			}
			
			if (!bot.IsConnectedAndLoggedOn) {
				return BadRequest(new GenericResponse(false, ArchiSteamFarm.Localization.Strings.BotNotConnected));
			}

			HashSet<uint> apps = new();
			foreach (string appIDString in appIDs.Split(",", StringSplitOptions.RemoveEmptyEntries)) {
				if (!uint.TryParse(appIDString, out uint appID)) {
					return BadRequest(new GenericResponse(false, String.Format(ArchiSteamFarm.Localization.Strings.ErrorParsingObject, nameof(appIDString))));
				}

				apps.Add(appID);
			}

			if (apps.Count == 0) {
				return BadRequest(new GenericResponse(false, String.Format(ArchiSteamFarm.Localization.Strings.ErrorIsEmpty, nameof(appIDs))));
			}

			SteamApps.FreeLicenseCallback response;
			try {
				response = await bot.SteamApps.RequestFreeLicense(apps).ToLongRunningTask().ConfigureAwait(false);			
			} catch (Exception e) {
				bot.ArchiLogger.LogGenericWarningException(e);

				return BadRequest(new GenericResponse(false, e.Message));
			}

			return Ok(new GenericResponse<SteamApps.FreeLicenseCallback>(true, response));
		}

		[HttpGet("{botName:required}/RequestFreeSubLicense")]
		[HttpPost("{botName:required}/RequestFreeSubLicense")]
		[EndpointSummary("Request a free license for given subid")]
		[ProducesResponseType(typeof(GenericResponse<FreeSubResponse>), (int) HttpStatusCode.OK)]
		[ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.BadRequest)]
		public async Task<ActionResult<GenericResponse>> RequestFreeSubLicense(string botName, [FromQuery] uint subID) {
			if (string.IsNullOrEmpty(botName)) {
				throw new ArgumentNullException(nameof(botName));
			}

			Bot? bot = Bot.GetBot(botName);
			if (bot == null) {
				return BadRequest(new GenericResponse(false, string.Format(ArchiSteamFarm.Localization.Strings.BotNotFound, botName)));
			}
			
			if (!bot.IsConnectedAndLoggedOn) {
				return BadRequest(new GenericResponse(false, ArchiSteamFarm.Localization.Strings.BotNotConnected));
			}

			EResult result;
			EPurchaseResultDetail purchaseResult;
			try {
				(result, purchaseResult) = await bot.Actions.AddFreeLicensePackage(subID).ConfigureAwait(false);
			} catch (Exception e) {
				bot.ArchiLogger.LogGenericWarningException(e);

				return BadRequest(new GenericResponse(false, e.Message));
			}

			return Ok(new GenericResponse<FreeSubResponse>(true, new FreeSubResponse(result, purchaseResult)));
		}

		[HttpGet("{botName:required}/GetOwnedPackages")]
		[EndpointSummary("Retrieves all packages owned by the given bot")]
		[ProducesResponseType(typeof(GenericResponse<IEnumerable<uint>>), (int) HttpStatusCode.OK)]
		[ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.BadRequest)]
		public ActionResult<GenericResponse> GetOwnedPackages(string botName) {
			if (string.IsNullOrEmpty(botName)) {
				throw new ArgumentNullException(nameof(botName));
			}

			Bot? bot = Bot.GetBot(botName);
			if (bot == null) {
				return BadRequest(new GenericResponse(false, string.Format(ArchiSteamFarm.Localization.Strings.BotNotFound, botName)));
			}

			if (bot.OwnedPackages.Count == 0) {
				return BadRequest(new GenericResponse(false, Strings.NoPackagesFound));
			}

			return Ok(new GenericResponse<IEnumerable<uint>>(true, bot.OwnedPackages.Keys));
		}

		[HttpGet("{botName:required}/GetOwnedApps")]
		[EndpointSummary("Retrieves all apps owned by the given bot")]
		[ProducesResponseType(typeof(GenericResponse<IEnumerable<uint>>), (int) HttpStatusCode.OK)]
		[ProducesResponseType(typeof(GenericResponse<Dictionary<uint, CPlayer_GetOwnedGames_Response.Game?>>), (int) HttpStatusCode.OK)]
		[ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.BadRequest)]
		public async Task<ActionResult<GenericResponse>> GetOwnedApps(string botName, bool showDetails = false) {
			if (string.IsNullOrEmpty(botName)) {
				throw new ArgumentNullException(nameof(botName));
			}

			Bot? bot = Bot.GetBot(botName);
			if (bot == null) {
				return BadRequest(new GenericResponse(false, String.Format(ArchiSteamFarm.Localization.Strings.BotNotFound, botName)));
			}

			if (bot.OwnedPackages.Count == 0) {
				return BadRequest(new GenericResponse(false, Strings.NoAppsFound));
			}

			if (ASF.GlobalDatabase == null) {
				return BadRequest(new GenericResponse(false, String.Format(ArchiSteamFarm.Localization.Strings.ErrorObjectIsNull, nameof(ASF.GlobalDatabase))));
			}

			var ownedPackageIDs = bot.OwnedPackages.Keys.ToHashSet();
			var ownedAppIDs = ASF.GlobalDatabase!.PackagesDataReadOnly.Where(x => ownedPackageIDs.Contains(x.Key) && x.Value.AppIDs != null).SelectMany(x => x.Value.AppIDs!).ToHashSet().ToList();
			ownedAppIDs.Sort();

			if (showDetails) {
				Dictionary<uint, CPlayer_GetOwnedGames_Response.Game>? detailsList;
				try {
					detailsList = await SteamHandler.Handlers[bot.BotName].GetOwnedGames(bot.SteamID).ConfigureAwait(false);

					if (detailsList == null) {
						return BadRequest(new GenericResponse(false, Strings.AppListFetchFailed));
					}
				}
				catch (Exception e) {
					return BadRequest(new GenericResponse(false, e.Message));
				}

				return Ok(new GenericResponse<Dictionary<uint, CPlayer_GetOwnedGames_Response.Game?>>(true, ownedAppIDs.ToDictionary(appID => appID, appID => {
					if (detailsList.TryGetValue(appID, out CPlayer_GetOwnedGames_Response.Game? game)) {
						return game;
					}

					return null;
				})));
			}

			return Ok(new GenericResponse<IEnumerable<uint>>(true, ownedAppIDs));
		}

		[Consumes("application/json")]
		[HttpPost("{botNames:required}/QueueLicenses")]
		[EndpointSummary("Adds the provided appids and subids to the given bot's package queue")]
		[ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.OK)]
		[ProducesResponseType(typeof(GenericResponse), (int) HttpStatusCode.BadRequest)]
		public ActionResult<GenericResponse> QueueLicenses(string botNames, [FromBody] QueueLicensesRequest request) {
			if (string.IsNullOrEmpty(botNames)) {
				throw new ArgumentNullException(nameof(botNames));
			}

			HashSet<Bot>? bots = Bot.GetBots(botNames);
			if (bots == null || bots.Count == 0) {
				return BadRequest(new GenericResponse(false, String.Format(ArchiSteamFarm.Localization.Strings.BotNotFound, botNames)));
			}

			if (PackageHandler.Handlers.Keys.Union(bots.Select(x => x.BotName)).Count() == 0) {
				return BadRequest(new GenericResponse(false, Strings.PluginNotEnabled));
			}

			foreach (Bot bot in bots) {
				if (!PackageHandler.Handlers.Keys.Contains(bot.BotName)) {
					continue;
				}

				PackageHandler.Handlers[bot.BotName].AddPackages(request.AppIDs, request.PackageIDs, request.UseFilter);
			}

			Utilities.InBackground(async() => await PackageHandler.HandleChanges().ConfigureAwait(false));

			return Ok(new GenericResponse(true));
		}
	}
}


================================================
FILE: FreePackages/IPC/Requests/QueueLicensesRequest.cs
================================================
using System.Collections.Generic;
using System.Text.Json.Serialization;

namespace FreePackages.IPC {
	public sealed class QueueLicensesRequest {
		[JsonInclude]
		public HashSet<uint>? AppIDs { get; private init; } = null;

		[JsonInclude]
		public HashSet<uint>? PackageIDs { get; private init; } = null;

		[JsonInclude]
		public bool UseFilter { get; private init; } = true;

		[JsonConstructor]
		private QueueLicensesRequest() { }
	}
}

================================================
FILE: FreePackages/IPC/Responses/FreeSubResponse.cs
================================================
using System.Text.Json.Serialization;
using SteamKit2;

namespace FreePackages.IPC {
	public sealed class FreeSubResponse {
		[JsonInclude]
		[JsonPropertyName("Result")]
		public EResult Result { get; private init; }

		[JsonInclude]
		[JsonPropertyName("PurchaseResultDetail")]
		public EPurchaseResultDetail PurchaseResultDetail { get; private init; }

		public FreeSubResponse(EResult result, EPurchaseResultDetail purchaseResultDetail) {
			Result = result;
			PurchaseResultDetail = purchaseResultDetail;
		}
	}
}

================================================
FILE: FreePackages/Json.cs
================================================
using System;
using System.Collections.Generic;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace FreePackages {
	internal static class Steam {
		internal sealed class PlaytestAccessResponse {
			[JsonInclude]
			[JsonPropertyName("granted")]
			[JsonRequired]
			internal int? Granted  { get; private init; } = null;

			[JsonInclude]
			[JsonPropertyName("success")]
			[JsonRequired]
			internal int Success  { get; private init; } = 0;

			[JsonConstructor]
			internal PlaytestAccessResponse() {}
		}

		internal sealed class UserData {
			[JsonInclude]
			[JsonPropertyName("rgOwnedPackages")]
			[JsonRequired]
			internal HashSet<uint> OwnedPackages { get; private init; } = new();

			[JsonInclude]
			[JsonPropertyName("rgOwnedApps")]
			[JsonRequired]
			internal HashSet<uint> OwnedApps { get; private init; } = new();

			[JsonInclude]
			[JsonPropertyName("rgIgnoredApps")]
			[JsonRequired]
			[JsonConverter(typeof(EmptyArrayOrDictionaryConverter))]
			internal Dictionary<uint, uint> IgnoredApps { get; private init; } = new();

			[JsonInclude]
			[JsonPropertyName("rgExcludedTags")]
			[JsonRequired]
			internal List<Tag> ExcludedTags { get; private init; } = new();

			[JsonInclude]
			[JsonPropertyName("rgExcludedContentDescriptorIDs")]
			[JsonRequired]
			internal HashSet<uint> ExcludedContentDescriptorIDs { get; private init; } = new();

			[JsonInclude]
			[JsonPropertyName("rgWishlist")]
			[JsonRequired]
			internal HashSet<uint> WishlistedApps { get; private init; } = new();

			[JsonInclude]
			[JsonPropertyName("rgFollowedApps")]
			[JsonRequired]
			internal HashSet<uint> FollowedApps { get; private init; } = new();

			[JsonExtensionData]
			[JsonInclude]
			internal Dictionary<string, JsonElement> AdditionalData { get; private init; } = new();

			[JsonConstructor]
			internal UserData() {}
		}

		internal sealed class Tag {
			[JsonInclude]
			[JsonPropertyName("tagid")]
			[JsonRequired]
			internal uint TagID = 0;

			[JsonInclude]
			[JsonPropertyName("name")]
			[JsonRequired]
			internal string Name = "";

			[JsonInclude]
			[JsonPropertyName("timestamp_added")]
			[JsonRequired]
			internal uint TimestampAdded = 0;

			[JsonConstructor]
			internal Tag() {}
		}

		internal sealed class UserInfo {
			[JsonInclude]
			[JsonPropertyName("logged_in")]
			public bool LoggedIn;

			[JsonInclude]
			[JsonPropertyName("steamid")]
			public string SteamID = "";

			[JsonInclude]
			[JsonPropertyName("accountid")]
			public int AccountID;

			[JsonInclude]
			[JsonPropertyName("account_name")]
			public string AccountName = "";

			[JsonInclude]
			[JsonPropertyName("is_support")]
			public bool IsSupport;

			[JsonInclude]
			[JsonPropertyName("is_limited")]
			public bool IsLimited;

			[JsonInclude]
			[JsonPropertyName("is_partner_member")]
			public bool IsPartnerMember;

			[JsonInclude]
			[JsonPropertyName("is_valve_email")]
			public bool IsValveEmail;

			[JsonInclude]
			[JsonPropertyName("country_code")]
			[JsonRequired]
			public string CountryCode = "";

			[JsonInclude]
			[JsonPropertyName("excluded_content_descriptors")]
			public HashSet<uint> ExcludedContentDescriptors = new();

			[JsonConstructor]
			internal UserInfo() {}
		}

		// https://stackoverflow.com/questions/12221950/how-to-deserialize-object-that-can-be-an-array-or-a-dictionary-with-newtonsoft
		public class EmptyArrayOrDictionaryConverter : JsonConverter<Dictionary<uint, uint>> {
			public override Dictionary<uint, uint> Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) {
				if (reader.TokenType == JsonTokenType.StartObject) {
					var dictionary = JsonSerializer.Deserialize<Dictionary<uint, uint>>(ref reader, options);
					if (dictionary == null) {
						throw new JsonException();
					}

					return dictionary;
				} else if (reader.TokenType == JsonTokenType.StartArray) {
					reader.Read();
					if (reader.TokenType == JsonTokenType.EndArray) {
						return new Dictionary<uint, uint>();
					}
				}

				throw new JsonException();
			}

			public override void Write(Utf8JsonWriter writer, Dictionary<uint, uint> value, JsonSerializerOptions options) {
				throw new NotImplementedException();
			}
		}
	}
}


================================================
FILE: FreePackages/Localization/README.md
================================================
If you'd like to help translate this plugin you can do so here: https://crowdin.com/project/freepackages

Contact me on Crowdin if your language isn't currently listed


================================================
FILE: FreePackages/Localization/Strings.de-DE.resx
================================================
<?xml version="1.0" encoding="utf-8"?>
<root>
  <!--
    Microsoft ResX Schema

    Version 2.0

    The primary goals of this format is to allow a simple XML format
    that is mostly human readable. The generation and parsing of the
    various data types are done through the TypeConverter classes
    associated with the data types.

    Example:

    ... ado.net/XML headers & schema ...
    <resheader name="resmimetype">text/microsoft-resx</resheader>
    <resheader name="version">2.0</resheader>
    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
        <value>[base64 mime encoded serialized .NET Framework object]</value>
    </data>
    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
        <comment>This is a comment</comment>
    </data>

    There are any number of "resheader" rows that contain simple
    name/value pairs.

    Each data row contains a name, and value. The row also contains a
    type or mimetype. Type corresponds to a .NET class that support
    text/value conversion through the TypeConverter architecture.
    Classes that don't support this are serialized and stored with the
    mimetype set.

    The mimetype is used for serialized objects, and tells the
    ResXResourceReader how to depersist the object. This is currently not
    extensible. For a given mimetype the value must be set accordingly:

    Note - application/x-microsoft.net.object.binary.base64 is the format
    that the ResXResourceWriter will generate, however the reader can
    read any of the formats listed below.

    mimetype: application/x-microsoft.net.object.binary.base64
    value   : The object must be serialized with
            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
            : and then encoded with base64 encoding.

    mimetype: application/x-microsoft.net.object.soap.base64
    value   : The object must be serialized with
            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
            : and then encoded with base64 encoding.

    mimetype: application/x-microsoft.net.object.bytearray.base64
    value   : The object must be serialized into a byte array
            : using a System.ComponentModel.TypeConverter
            : and then encoded with base64 encoding.
    -->
  <xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
    <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
    <xsd:element name="root" msdata:IsDataSet="true">
      <xsd:complexType>
        <xsd:choice maxOccurs="unbounded">
          <xsd:element name="metadata">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0"/>
              </xsd:sequence>
              <xsd:attribute name="name" use="required" type="xsd:string"/>
              <xsd:attribute name="type" type="xsd:string"/>
              <xsd:attribute name="mimetype" type="xsd:string"/>
              <xsd:attribute ref="xml:space"/>
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="assembly">
            <xsd:complexType>
              <xsd:attribute name="alias" type="xsd:string"/>
              <xsd:attribute name="name" type="xsd:string"/>
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="data">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
              <xsd:attribute ref="xml:space"/>
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="resheader">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required"/>
            </xsd:complexType>
          </xsd:element>
        </xsd:choice>
      </xsd:complexType>
    </xsd:element>
  </xsd:schema>
  <resheader name="resmimetype">
    <value>text/microsoft-resx</value>
  </resheader>
  <resheader name="version">
    <value>2.0</value>
  </resheader>
  <resheader name="reader">
    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <resheader name="writer">
    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <data name="BadgeDataFetchFailed" xml:space="preserve">
    <value>Abruf der Abzeichendaten für kostenlose Pakete fehlgeschlagen</value>
    <comment/>
  </data>
  <data name="BadgeDataParsingFailed" xml:space="preserve">
    <value>Parsen der Abzeichendaten für kostenlose Pakete fehlgeschlagen</value>
    <comment/>
  </data>
  <data name="PlaytestConfigLimitTriggered" xml:space="preserve">
    <value>PlaytestMode auf 0 (keiner) geändert, nur 1 Bot darf diesen Filter verwenden</value>
    <comment/>
  </data>
  <data name="QueueEmpty" xml:space="preserve">
    <value>Warteschlange ist leer</value>
    <comment/>
  </data>
  <data name="PackagesRemoved" xml:space="preserve">
    <value>{0} kostenlose Pakete entfernt.</value>
    <comment>{0} will be replaced by a number</comment>
  </data>
  <data name="DiscoveredAppsRemoved" xml:space="preserve">
    <value>{0} entdeckte Apps entfernt.</value>
    <comment>{0} will be replaced by a number</comment>
  </data>
  <data name="DiscoveredPackagesRemoved" xml:space="preserve">
    <value>{0} entdeckte Pakete entfernt.</value>
    <comment>{0} will be replaced by a number</comment>
  </data>
  <data name="DiscoveredAppsAdded" xml:space="preserve">
    <value>{0} zur Warteschlange für entdeckte Apps hinzugefügt</value>
    <comment>{0} will be replaced by an appID</comment>
  </data>
  <data name="DiscoveredPackagesAdded" xml:space="preserve">
    <value>{0} zur Warteschlange für entdeckte Pakete hinzugefügt</value>
    <comment>{0} will be replaced by a subID</comment>
  </data>
  <data name="AppsQueued" xml:space="preserve">
    <value>{0} zur Warteschlange für kostenlose Pakete hinzugefügt</value>
    <comment>{0} will be replaced by an appID</comment>
  </data>
  <data name="PackagesQueued" xml:space="preserve">
    <value>{0} zur Warteschlange für kostenlose Pakete hinzugefügt</value>
    <comment>{0} will be replaced by a subID</comment>
  </data>
  <data name="ActivationPaused" xml:space="preserve">
    <value>Aktivierung kostenloser Pakete pausiert bis {0}</value>
    <comment>{0} will be replaced by a time</comment>
  </data>
  <data name="RateLimitExceeded" xml:space="preserve">
    <value>Aktivierungslimit überschritten</value>
    <comment/>
  </data>
  <data name="ReplacedWith" xml:space="preserve">
    <value>Ersetzt durch {0}</value>
    <comment>{0} will be replaced by a number</comment>
  </data>
  <data name="Unknown" xml:space="preserve">
    <value>Unbekannt</value>
    <comment/>
  </data>
  <data name="Invalid" xml:space="preserve">
    <value>Ungültig</value>
    <comment/>
  </data>
  <data name="Failed" xml:space="preserve">
    <value>Fehlgeschlagen</value>
    <comment/>
  </data>
  <data name="Waitlisted" xml:space="preserve">
    <value>Auf der Warteliste</value>
    <comment/>
  </data>
  <data name="QueueStatus" xml:space="preserve">
    <value>{0} kostenlose Pakete in der Warteschlange. {1}/{2} Aktivierungen verwendet.</value>
    <comment>{0} will be replaced by a number, {1} will be replaced by a number, {2} will be replaced by a number</comment>
  </data>
  <data name="QueuePausedWhileIngame" xml:space="preserve">
    <value>Die Aktivierung wurde pausiert, da das Konto zum Spielen eines Spiels verwendet wird.</value>
    <comment/>
  </data>
  <data name="QueueLimitedUntil" xml:space="preserve">
    <value>Die Aktivierung wird um {0} fortgesetzt.</value>
    <comment>{0} will be replaced by a time</comment>
  </data>
  <data name="QueueDiscoveryStatus" xml:space="preserve">
    <value>{0} Apps und {1} Pakete entdeckt, aber noch nicht bearbeitet.</value>
    <comment>{0} will be replaced by a number, {1} will be replaced by a number</comment>
  </data>
  <data name="PICSRestart" xml:space="preserve">
    <value>PICS neu gestartet, überspringe von der Nummer {0} bis {1}</value>
    <comment>{0} will be replaced by a number, {1} will be replaced by a number</comment>
  </data>
  <data name="MissedApps" xml:space="preserve">
    <value>Möglicherweise wurden einige kostenlose Apps aufgrund eines PICS-Neustarts verpasst</value>
    <comment/>
  </data>
  <data name="RecoveredApps" xml:space="preserve">
    <value>{0} Appänderungen bei Änderungsnummer {1} wiederhergestellt</value>
    <comment>{0} will be replaced by a number, {1} will be replaced by a number</comment>
  </data>
  <data name="MissedPackages" xml:space="preserve">
    <value>Möglicherweise wurden einige kostenlose Pakete aufgrund eines PICS-Neustarts verpasst</value>
    <comment/>
  </data>
  <data name="RecoveredPackages" xml:space="preserve">
    <value>{0} Paketänderungen bei Änderungsnummer {1} wiederhergestellt</value>
    <comment>{0} will be replaced by a number, {1} will be replaced by a number</comment>
  </data>
  <data name="NoPackagesFound" xml:space="preserve">
    <value>Keine Pakete gefunden</value>
    <comment/>
  </data>
  <data name="NoAppsFound" xml:space="preserve">
    <value>Keine Apps gefunden</value>
    <comment/>
  </data>
  <data name="AppListFetchFailed" xml:space="preserve">
    <value>App-Liste konnte nicht abgerufen werden</value>
    <comment/>
  </data>
  <data name="PluginNotEnabled" xml:space="preserve">
    <value>Plugin Free Packages nicht aktiviert</value>
    <comment/>
  </data>
  <data name="ASFInfoParseFailed" xml:space="preserve">
    <value>Fehler beim Parsen der Daten von ASFInfo</value>
    <comment/>
  </data>
</root>

================================================
FILE: FreePackages/Localization/Strings.resx
================================================
<?xml version="1.0" encoding="utf-8"?>
<root>
  <!--  
    Microsoft ResX Schema 
    
    Version 2.0
    
    The primary goals of this format is to allow a simple XML format 
    that is mostly human readable. The generation and parsing of the 
    various data types are done through the TypeConverter classes 
    associated with the data types.
    
    Example:
    
    ... ado.net/XML headers & schema ...
    <resheader name="resmimetype">text/microsoft-resx</resheader>
    <resheader name="version">2.0</resheader>
    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
        <value>[base64 mime encoded serialized .NET Framework object]</value>
    </data>
    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
        <comment>This is a comment</comment>
    </data>
                
    There are any number of "resheader" rows that contain simple 
    name/value pairs.
    
    Each data row contains a name, and value. The row also contains a 
    type or mimetype. Type corresponds to a .NET class that support 
    text/value conversion through the TypeConverter architecture. 
    Classes that don't support this are serialized and stored with the 
    mimetype set.
    
    The mimetype is used for serialized objects, and tells the 
    ResXResourceReader how to depersist the object. This is currently not 
    extensible. For a given mimetype the value must be set accordingly:
    
    Note - application/x-microsoft.net.object.binary.base64 is the format 
    that the ResXResourceWriter will generate, however the reader can 
    read any of the formats listed below.
    
    mimetype: application/x-microsoft.net.object.binary.base64
    value   : The object must be serialized with 
            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
            : and then encoded with base64 encoding.
    
    mimetype: application/x-microsoft.net.object.soap.base64
    value   : The object must be serialized with 
            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
            : and then encoded with base64 encoding.

    mimetype: application/x-microsoft.net.object.bytearray.base64
    value   : The object must be serialized into a byte array 
            : using a System.ComponentModel.TypeConverter
            : and then encoded with base64 encoding.
    -->
  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
    <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
    <xsd:element name="root" msdata:IsDataSet="true">
      <xsd:complexType>
        <xsd:choice maxOccurs="unbounded">
          <xsd:element name="metadata">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" />
              </xsd:sequence>
              <xsd:attribute name="name" use="required" type="xsd:string" />
              <xsd:attribute name="type" type="xsd:string" />
              <xsd:attribute name="mimetype" type="xsd:string" />
              <xsd:attribute ref="xml:space" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="assembly">
            <xsd:complexType>
              <xsd:attribute name="alias" type="xsd:string" />
              <xsd:attribute name="name" type="xsd:string" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="data">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
              <xsd:attribute ref="xml:space" />
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="resheader">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" />
            </xsd:complexType>
          </xsd:element>
        </xsd:choice>
      </xsd:complexType>
    </xsd:element>
  </xsd:schema>
  <resheader name="resmimetype">
    <value>text/microsoft-resx</value>
  </resheader>
  <resheader name="version">
    <value>2.0</value>
  </resheader>
  <resheader name="reader">
    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <resheader name="writer">
    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <data name="BadgeDataFetchFailed" xml:space="preserve">
    <value>Failed to fetch badge data for free packages</value>
  </data>
  <data name="BadgeDataParsingFailed" xml:space="preserve">
    <value>Failed to parse badge data for free packages</value>
  </data>
  <data name="PlaytestConfigLimitTriggered" xml:space="preserve">
    <value>Changed PlaytestMode to 0 (None), only 1 bot is allowed to use this filter</value>
  </data>
  <data name="QueueEmpty" xml:space="preserve">
    <value>Queue is empty</value>
  </data>
  <data name="PackagesRemoved" xml:space="preserve">
    <value>{0} free packages removed.</value>
    <comment>{0} will be replaced by a number</comment>
  </data>
  <data name="DiscoveredAppsRemoved" xml:space="preserve">
    <value>{0} discovered apps removed.</value>
    <comment>{0} will be replaced by a number</comment>
  </data>
  <data name="DiscoveredPackagesRemoved" xml:space="preserve">
    <value>{0} discovered packages removed.</value>
    <comment>{0} will be replaced by a number</comment>
  </data>
  <data name="DiscoveredAppsAdded" xml:space="preserve">
    <value>Added {0} to discovered apps queue</value>
    <comment>{0} will be replaced by an appID</comment>
  </data>
  <data name="DiscoveredPackagesAdded" xml:space="preserve">
    <value>Added {0} to discovered packages queue</value>
    <comment>{0} will be replaced by a subID</comment>
  </data>
  <data name="AppsQueued" xml:space="preserve">
    <value>Added {0} to free packages queue</value>
    <comment>{0} will be replaced by an appID</comment>
  </data>
  <data name="PackagesQueued" xml:space="preserve">
    <value>Added {0} to free packages queue</value>
    <comment>{0} will be replaced by a subID</comment>
  </data>
  <data name="ActivationPaused" xml:space="preserve">
    <value>Pausing free package activations until {0}</value>
    <comment>{0} will be replaced by a time</comment>
  </data>
  <data name="RateLimitExceeded" xml:space="preserve">
    <value>Free Package rate limit exceeded</value>
  </data>
  <data name="ReplacedWith" xml:space="preserve">
    <value>Replaced with {0}</value>
    <comment>{0} will be replaced by a number</comment>
  </data>
  <data name="Unknown" xml:space="preserve">
    <value>Unknown</value>
  </data>
  <data name="Invalid" xml:space="preserve">
    <value>Invalid</value>
  </data>
  <data name="Failed" xml:space="preserve">
    <value>Failed</value>
  </data>
  <data name="Waitlisted" xml:space="preserve">
    <value>Waitlisted</value>
  </data>
  <data name="QueueStatus" xml:space="preserve">
    <value>{0} free packages queued. {1}/{2} activations used.</value>
    <comment>{0} will be replaced by a number, {1} will be replaced by a number, {2} will be replaced by a number</comment>
  </data>
  <data name="QueuePausedWhileIngame" xml:space="preserve">
    <value>Activations are now paused as the account is being used to play a game.</value>
  </data>
  <data name="QueueLimitedUntil" xml:space="preserve">
    <value>Activations will resume at {0}.</value>
    <comment>{0} will be replaced by a time</comment>
  </data>
  <data name="QueueDiscoveryStatus" xml:space="preserve">
    <value>{0} apps and {1} packages discovered but not processed yet.</value>
    <comment>{0} will be replaced by a number, {1} will be replaced by a number</comment>
  </data>
  <data name="PICSRestart" xml:space="preserve">
    <value>PICS restarted, skipping from change number {0} to {1}</value>
    <comment>{0} will be replaced by a number, {1} will be replaced by a number</comment>
  </data>
  <data name="MissedApps" xml:space="preserve">
    <value>Possibly missed some free apps due to PICS restart</value>
  </data>
  <data name="RecoveredApps" xml:space="preserve">
    <value>Recovered {0} app changes at change number {1}</value>
    <comment>{0} will be replaced by a number, {1} will be replaced by a number</comment>
  </data>
  <data name="MissedPackages" xml:space="preserve">
    <value>Possibly missed some free packages due to PICS restart</value>
  </data>
  <data name="RecoveredPackages" xml:space="preserve">
    <value>Recovered {0} package changes at change number {1}</value>
    <comment>{0} will be replaced by a number, {1} will be replaced by a number</comment>
  </data>
  <data name="NoPackagesFound" xml:space="preserve">
    <value>No packages found</value>
  </data>
  <data name="NoAppsFound" xml:space="preserve">
    <value>No apps found</value>
  </data>
  <data name="AppListFetchFailed" xml:space="preserve">
    <value>Failed to get app list</value>
  </data>
  <data name="PluginNotEnabled" xml:space="preserve">
    <value>Free Packages plugin not enabled</value>
  </data>
  <data name="ASFInfoParseFailed" xml:space="preserve">
    <value>Failed to parse data from ASFInfo</value>
  </data>
  <data name="RemovingPackages" xml:space="preserve">
    <value>Removing {0} packages.</value>
    <comment>{0} will be replaced by a number</comment>
  </data>
  <data name="LicensePageFetchFail" xml:space="preserve">
    <value>Failed to fetch licenses page</value>
    <comment>
</comment>
  </data>
  <data name="LicensePageEmpty" xml:space="preserve">
    <value>Failed to find any removable packages</value>
    <comment>
</comment>
  </data>
  <data name="RemovingNoPackages" xml:space="preserve">
    <value>Didn't find any free packages to remove</value>
    <comment>
</comment>
  </data>
  <data name="ProductInfoFetchFailed" xml:space="preserve">
    <value>Failed to fetch product info</value>
    <comment>
</comment>
  </data>
  <data name="RemovalQueueEmpty" xml:space="preserve">
    <value>No packages are being removed</value>
  </data>
  <data name="RemovalsCancelled" xml:space="preserve">
    <value>Cancelled {0} removals.</value>
    <comment>{0} will be replaced by a number</comment>
  </data>
  <data name="RemovalWaitMessage" xml:space="preserve">
    <value>Looking for free packages to remove, this scan will take ~{0} minutes. Cancel at any time using: {1}</value>
    <comment>{0} will be replaced by a number, {1} will be replaced with a command</comment>
  </data>
  <data name="RemovablePackagesFound" xml:space="preserve">
    <value>Found {0} packages to remove: 
{1} 
To cancel, use: {2}
To continue with removal, use: {3}
To remove items from the list above before continuing, use: {4}</value>
    <comment>{0} will be replaced by a number, {1} will be replaced by a list, {2} will be replaced with a command, {3} will be replaced with a command, {4} will be replaced with a command</comment>
  </data>
  <data name="RemovingNoUnwatedPackages" xml:space="preserve">
    <value>Didn't find any unwanted free packages to remove</value>
    <comment>
</comment>
  </data>
  <data name="RemovalScanCancelled" xml:space="preserve">
    <value>Scan for unwanted free packages cancelled.</value>
    <comment>
</comment>
  </data>
  <data name="RemovalScanAlreadyRunning" xml:space="preserve">
    <value>Already scanning for unwanted free packages</value>
    <comment>
</comment>
  </data>
  <data name="RemovalPackageNotFound" xml:space="preserve">
    <value>{0} was not in the list of scanned packages</value>
    <comment>{0} will be replaced by a packageID</comment>
  </data>
  <data name="RemovalPackageCancelled" xml:space="preserve">
    <value>{0} removed</value>
    <comment>{0} will be replaced by a packageID</comment>
  </data>
  <data name="RemovalScanCanceling" xml:space="preserve">
    <value>Cancelling scan for unwanted free packages...</value>
    <comment>
</comment>
  </data>
  <data name="RemovalScanNeeded" xml:space="preserve">
    <value>You must first scan for unwanted free packages using the command: {0}</value>
    <comment>{0} will be replaced by a command</comment>
  </data>
  <data name="RemovalsPaused" xml:space="preserve">
    <value>Pausing package removals until {0}</value>
    <comment>{0} will be replaced by a time</comment>
  </data>
  <data name="PlaytimeFetchFailed" xml:space="preserve">
    <value>Failed to get playtime information</value>
    <comment>
</comment>
  </data>
  <data name="Unwanted" xml:space="preserve">
    <value>Unwanted</value>
    <comment>
</comment>
  </data>
</root>


================================================
FILE: FreePackages/Localization/Strings.ru-RU.resx
================================================
<?xml version="1.0" encoding="utf-8"?>
<root>
  <!--  
    Microsoft ResX Schema 
    
    Version 2.0
    
    The primary goals of this format is to allow a simple XML format 
    that is mostly human readable. The generation and parsing of the 
    various data types are done through the TypeConverter classes 
    associated with the data types.
    
    Example:
    
    ... ado.net/XML headers & schema ...
    <resheader name="resmimetype">text/microsoft-resx</resheader>
    <resheader name="version">2.0</resheader>
    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
        <value>[base64 mime encoded serialized .NET Framework object]</value>
    </data>
    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
        <comment>This is a comment</comment>
    </data>
                
    There are any number of "resheader" rows that contain simple 
    name/value pairs.
    
    Each data row contains a name, and value. The row also contains a 
    type or mimetype. Type corresponds to a .NET class that support 
    text/value conversion through the TypeConverter architecture. 
    Classes that don't support this are serialized and stored with the 
    mimetype set.
    
    The mimetype is used for serialized objects, and tells the 
    ResXResourceReader how to depersist the object. This is currently not 
    extensible. For a given mimetype the value must be set accordingly:
    
    Note - application/x-microsoft.net.object.binary.base64 is the format 
    that the ResXResourceWriter will generate, however the reader can 
    read any of the formats listed below.
    
    mimetype: application/x-microsoft.net.object.binary.base64
    value   : The object must be serialized with 
            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
            : and then encoded with base64 encoding.
    
    mimetype: application/x-microsoft.net.object.soap.base64
    value   : The object must be serialized with 
            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
            : and then encoded with base64 encoding.

    mimetype: application/x-microsoft.net.object.bytearray.base64
    value   : The object must be serialized into a byte array 
            : using a System.ComponentModel.TypeConverter
            : and then encoded with base64 encoding.
    -->
  <xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
    <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
    <xsd:element name="root" msdata:IsDataSet="true">
      <xsd:complexType>
        <xsd:choice maxOccurs="unbounded">
          <xsd:element name="metadata">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0"/>
              </xsd:sequence>
              <xsd:attribute name="name" use="required" type="xsd:string"/>
              <xsd:attribute name="type" type="xsd:string"/>
              <xsd:attribute name="mimetype" type="xsd:string"/>
              <xsd:attribute ref="xml:space"/>
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="assembly">
            <xsd:complexType>
              <xsd:attribute name="alias" type="xsd:string"/>
              <xsd:attribute name="name" type="xsd:string"/>
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="data">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
              <xsd:attribute ref="xml:space"/>
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="resheader">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required"/>
            </xsd:complexType>
          </xsd:element>
        </xsd:choice>
      </xsd:complexType>
    </xsd:element>
  </xsd:schema>
  <resheader name="resmimetype">
    <value>text/microsoft-resx</value>
  </resheader>
  <resheader name="version">
    <value>2.0</value>
  </resheader>
  <resheader name="reader">
    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <resheader name="writer">
    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <data name="BadgeDataFetchFailed" xml:space="preserve">
    <value>Не удалось получить данные о значках для бесплатных пакетов</value>
  </data>
  <data name="BadgeDataParsingFailed" xml:space="preserve">
    <value>Не удалось собрать данные о значках для бесплатных пакетов</value>
  </data>
  <data name="PlaytestConfigLimitTriggered" xml:space="preserve">
    <value>Значение фильтра PlaytestMode было автоматически заменено на "0" (игнорировать всё), так как его может использовать только один бот</value>
  </data>
  <data name="QueueEmpty" xml:space="preserve">
    <value>Очередь пуста</value>
  </data>
  <data name="PackagesRemoved" xml:space="preserve">
    <value>Бесплатных пакетов удалено: {0}.</value>
    <comment>{0} will be replaced by a number</comment>
  </data>
  <data name="DiscoveredAppsRemoved" xml:space="preserve">
    <value>Обнаруженных приложений удалено: {0}.</value>
    <comment>{0} will be replaced by a number</comment>
  </data>
  <data name="DiscoveredPackagesRemoved" xml:space="preserve">
    <value>Обнаруженных пакетов удалено: {0}.</value>
    <comment>{0} will be replaced by a number</comment>
  </data>
  <data name="DiscoveredAppsAdded" xml:space="preserve">
    <value>Добавлено в очередь обнаруженных приложений: {0}</value>
    <comment>{0} will be replaced by an appID</comment>
  </data>
  <data name="DiscoveredPackagesAdded" xml:space="preserve">
    <value>Добавлено в очередь обнаруженных пакетов: {0}</value>
    <comment>{0} will be replaced by a subID</comment>
  </data>
  <data name="AppsQueued" xml:space="preserve">
    <value>Добавлено в очередь бесплатных пакетов: {0}</value>
    <comment>{0} will be replaced by an appID</comment>
  </data>
  <data name="PackagesQueued" xml:space="preserve">
    <value>Добавлено в очередь бесплатных пакетов: {0}</value>
    <comment>{0} will be replaced by a subID</comment>
  </data>
  <data name="ActivationPaused" xml:space="preserve">
    <value>Активация бесплатных пакетов приостановлена до {0}</value>
    <comment>{0} will be replaced by a time</comment>
  </data>
  <data name="RateLimitExceeded" xml:space="preserve">
    <value>Плагин "Free Package" превысил лимит активаций</value>
  </data>
  <data name="ReplacedWith" xml:space="preserve">
    <value>Заменено на {0}</value>
    <comment>{0} will be replaced by a number</comment>
  </data>
  <data name="Unknown" xml:space="preserve">
    <value>Неизвестно</value>
  </data>
  <data name="Invalid" xml:space="preserve">
    <value>Некорректно</value>
  </data>
  <data name="Failed" xml:space="preserve">
    <value>Неудачно</value>
  </data>
  <data name="Waitlisted" xml:space="preserve">
    <value>Ожидание</value>
  </data>
  <data name="QueueStatus" xml:space="preserve">
    <value>Бесплатных пакетов в очереди: {0}. Активаций использовано: {1}/{2}.</value>
    <comment>{0} will be replaced by a number, {1} will be replaced by a number, {2} will be replaced by a number</comment>
  </data>
  <data name="QueuePausedWhileIngame" xml:space="preserve">
    <value>Активации приостановлены, поскольку аккаунт используется для игры.</value>
  </data>
  <data name="QueueLimitedUntil" xml:space="preserve">
    <value>Активации продолжатся в {0}.</value>
    <comment>{0} will be replaced by a time</comment>
  </data>
  <data name="QueueDiscoveryStatus" xml:space="preserve">
    <value>Обнаружены, но ещё не обработаны приложения в количестве "{0}" и пакеты в количестве "{1}".</value>
    <comment>{0} will be replaced by a number, {1} will be replaced by a number</comment>
  </data>
  <data name="PICSRestart" xml:space="preserve">
    <value>PICS перезапущен. Переход с номера изменения {0} на {1}</value>
    <comment>{0} will be replaced by a number, {1} will be replaced by a number</comment>
  </data>
  <data name="MissedApps" xml:space="preserve">
    <value>Возможно, пропущены некоторые бесплатные приложения из-за перезагрузки PICS</value>
  </data>
  <data name="RecoveredApps" xml:space="preserve">
    <value>Восстановлены изменения приложений в количестве: "{0}" под изменённым номером {1}</value>
    <comment>{0} will be replaced by a number, {1} will be replaced by a number</comment>
  </data>
  <data name="MissedPackages" xml:space="preserve">
    <value>Возможно, пропущены некоторые бесплатные пакеты из-за перезагрузки PICS</value>
  </data>
  <data name="RecoveredPackages" xml:space="preserve">
    <value>Восстановлены изменения пакетов в количестве: "{0}" под изменённым номером {1}</value>
    <comment>{0} will be replaced by a number, {1} will be replaced by a number</comment>
  </data>
  <data name="NoPackagesFound" xml:space="preserve">
    <value>Пакеты не обнаружены</value>
  </data>
  <data name="NoAppsFound" xml:space="preserve">
    <value>Приложения не найдены</value>
  </data>
  <data name="AppListFetchFailed" xml:space="preserve">
    <value>Не удалось получить список приложений</value>
  </data>
  <data name="PluginNotEnabled" xml:space="preserve">
    <value>Плагин "Free Packages" не включён</value>
  </data>
  <data name="ASFInfoParseFailed" xml:space="preserve">
    <value>Не удалось собрать данные из ASFInfo</value>
  </data>
  <data name="RemovingPackages" xml:space="preserve">
    <value>Удалено пакетов: {0}.</value>
    <comment>{0} will be replaced by a number</comment>
  </data>
  <data name="LicensePageFetchFail" xml:space="preserve">
    <value>Не удалось получить страницу лицензий</value>
    <comment>
</comment>
  </data>
  <data name="LicensePageEmpty" xml:space="preserve">
    <value>Не удалось найти ни одного удаляемого пакета</value>
    <comment>
</comment>
  </data>
  <data name="RemovingNoPackages" xml:space="preserve">
    <value>Не найдены бесплатные пакеты для удаления</value>
    <comment>
</comment>
  </data>
  <data name="ProductInfoFetchFailed" xml:space="preserve">
    <value>Не удалось получить информацию о товаре</value>
    <comment>
</comment>
  </data>
  <data name="RemovalQueueEmpty" xml:space="preserve">
    <value>Ни один пакет не был удалён</value>
  </data>
  <data name="RemovalsCancelled" xml:space="preserve">
    <value>Отменённых удалений: {0}.</value>
    <comment>{0} will be replaced by a number</comment>
  </data>
  <data name="RemovalWaitMessage" xml:space="preserve">
    <value>Поиск бесплатных пакетов для удаления, сканирование займёт минут примерно: {0}. Можно отменить в любой момент командой: {1}</value>
    <comment>{0} will be replaced by a number, {1} will be replaced with a command</comment>
  </data>
  <data name="RemovablePackagesFound" xml:space="preserve">
    <value>Найдено пакетов для удаления - {0}: 
{1} 
Для отмены, используйте команду: {2}
Для продолжения удаления, используйте: {3}
Для удаления элементов из вышестоящего списка, прежде чем продолжить, используйте команду: {4}</value>
    <comment>{0} will be replaced by a number, {1} will be replaced by a list, {2} will be replaced with a command, {3} will be replaced with a command, {4} will be replaced with a command</comment>
  </data>
  <data name="RemovingNoUnwatedPackages" xml:space="preserve">
    <value>Не найдены нежелательные бесплатные пакеты для удаления</value>
    <comment>
</comment>
  </data>
  <data name="RemovalScanCancelled" xml:space="preserve">
    <value>Отменено сканирование нежелательных бесплатных пакетов.</value>
    <comment>
</comment>
  </data>
  <data name="RemovalScanAlreadyRunning" xml:space="preserve">
    <value>Уже идёт сканирование нежелательных бесплатных пакетов</value>
    <comment>
</comment>
  </data>
  <data name="RemovalPackageNotFound" xml:space="preserve">
    <value>Отсутствует в списке отсканированных пакетов: {0}</value>
    <comment>{0} will be replaced by a packageID</comment>
  </data>
  <data name="RemovalPackageCancelled" xml:space="preserve">
    <value>Удалено: {0}</value>
    <comment>{0} will be replaced by a packageID</comment>
  </data>
  <data name="RemovalScanCanceling" xml:space="preserve">
    <value>Отмена сканирования нежелательных бесплатных пакетов...</value>
    <comment>
</comment>
  </data>
  <data name="RemovalScanNeeded" xml:space="preserve">
    <value>Сначала необходимо просканировать нежелательные бесплатные пакеты с помощью команды: {0}</value>
    <comment>{0} will be replaced by a command</comment>
  </data>
  <data name="RemovalsPaused" xml:space="preserve">
    <value>Удаление пакетов приостановлено до {0}</value>
    <comment>{0} will be replaced by a time</comment>
  </data>
  <data name="PlaytimeFetchFailed" xml:space="preserve">
    <value>Не удалось получить информацию о наигранном времени</value>
    <comment>
</comment>
  </data>
</root>


================================================
FILE: FreePackages/Localization/Strings.tr-TR.resx
================================================
<?xml version="1.0" encoding="utf-8"?>
<root>
  <!--  
    Microsoft ResX Schema 
    
    Version 2.0
    
    The primary goals of this format is to allow a simple XML format 
    that is mostly human readable. The generation and parsing of the 
    various data types are done through the TypeConverter classes 
    associated with the data types.
    
    Example:
    
    ... ado.net/XML headers & schema ...
    <resheader name="resmimetype">text/microsoft-resx</resheader>
    <resheader name="version">2.0</resheader>
    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
        <value>[base64 mime encoded serialized .NET Framework object]</value>
    </data>
    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
        <comment>This is a comment</comment>
    </data>
                
    There are any number of "resheader" rows that contain simple 
    name/value pairs.
    
    Each data row contains a name, and value. The row also contains a 
    type or mimetype. Type corresponds to a .NET class that support 
    text/value conversion through the TypeConverter architecture. 
    Classes that don't support this are serialized and stored with the 
    mimetype set.
    
    The mimetype is used for serialized objects, and tells the 
    ResXResourceReader how to depersist the object. This is currently not 
    extensible. For a given mimetype the value must be set accordingly:
    
    Note - application/x-microsoft.net.object.binary.base64 is the format 
    that the ResXResourceWriter will generate, however the reader can 
    read any of the formats listed below.
    
    mimetype: application/x-microsoft.net.object.binary.base64
    value   : The object must be serialized with 
            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
            : and then encoded with base64 encoding.
    
    mimetype: application/x-microsoft.net.object.soap.base64
    value   : The object must be serialized with 
            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
            : and then encoded with base64 encoding.

    mimetype: application/x-microsoft.net.object.bytearray.base64
    value   : The object must be serialized into a byte array 
            : using a System.ComponentModel.TypeConverter
            : and then encoded with base64 encoding.
    -->
  <xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
    <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
    <xsd:element name="root" msdata:IsDataSet="true">
      <xsd:complexType>
        <xsd:choice maxOccurs="unbounded">
          <xsd:element name="metadata">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0"/>
              </xsd:sequence>
              <xsd:attribute name="name" use="required" type="xsd:string"/>
              <xsd:attribute name="type" type="xsd:string"/>
              <xsd:attribute name="mimetype" type="xsd:string"/>
              <xsd:attribute ref="xml:space"/>
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="assembly">
            <xsd:complexType>
              <xsd:attribute name="alias" type="xsd:string"/>
              <xsd:attribute name="name" type="xsd:string"/>
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="data">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
              <xsd:attribute ref="xml:space"/>
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="resheader">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required"/>
            </xsd:complexType>
          </xsd:element>
        </xsd:choice>
      </xsd:complexType>
    </xsd:element>
  </xsd:schema>
  <resheader name="resmimetype">
    <value>text/microsoft-resx</value>
  </resheader>
  <resheader name="version">
    <value>2.0</value>
  </resheader>
  <resheader name="reader">
    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <resheader name="writer">
    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <data name="BadgeDataFetchFailed" xml:space="preserve">
    <value>Ücretsiz paketlerin rozet verileri alınamadı</value>
  </data>
  <data name="BadgeDataParsingFailed" xml:space="preserve">
    <value>Ücretsiz paketlerin rozet verileri çözümlenemedi</value>
  </data>
  <data name="PlaytestConfigLimitTriggered" xml:space="preserve">
    <value>PlaytestMode 0 (Yok) olarak değiştirildi, bu filtreyi yalnızca 1 bot kullanabilir</value>
  </data>
  <data name="QueueEmpty" xml:space="preserve">
    <value>Kuyruk boş</value>
  </data>
  <data name="PackagesRemoved" xml:space="preserve">
    <value>{0} ücretsiz paket kaldırıldı.</value>
    <comment>{0} will be replaced by a number</comment>
  </data>
  <data name="DiscoveredAppsRemoved" xml:space="preserve">
    <value>{0} keşfedilen uygulama kaldırıldı.</value>
    <comment>{0} will be replaced by a number</comment>
  </data>
  <data name="DiscoveredPackagesRemoved" xml:space="preserve">
    <value>{0} keşfedilen paket kaldırıldı.</value>
    <comment>{0} will be replaced by a number</comment>
  </data>
  <data name="DiscoveredAppsAdded" xml:space="preserve">
    <value>{0} keşfedilen uygulamalar kuyruğuna eklendi</value>
    <comment>{0} will be replaced by an appID</comment>
  </data>
  <data name="DiscoveredPackagesAdded" xml:space="preserve">
    <value>{0} keşfedilen paketler kuyruğuna eklendi</value>
    <comment>{0} will be replaced by a subID</comment>
  </data>
  <data name="AppsQueued" xml:space="preserve">
    <value>{0} ücretsiz paket kuyruğuna eklendi</value>
    <comment>{0} will be replaced by an appID</comment>
  </data>
  <data name="PackagesQueued" xml:space="preserve">
    <value>{0} ücretsiz paket kuyruğuna eklendi</value>
    <comment>{0} will be replaced by a subID</comment>
  </data>
  <data name="ActivationPaused" xml:space="preserve">
    <value>Ücretsiz paket etkinleştirmeleri {0} tarihine kadar duraklatıldı</value>
    <comment>{0} will be replaced by a time</comment>
  </data>
  <data name="RateLimitExceeded" xml:space="preserve">
    <value>Ücretsiz Paket hız limiti aşıldı</value>
  </data>
  <data name="ReplacedWith" xml:space="preserve">
    <value>{0} ile değiştirildi</value>
    <comment>{0} will be replaced by a number</comment>
  </data>
  <data name="Unknown" xml:space="preserve">
    <value>Bilinmiyor</value>
  </data>
  <data name="Invalid" xml:space="preserve">
    <value>Geçersiz</value>
  </data>
  <data name="Failed" xml:space="preserve">
    <value>Başarısız</value>
  </data>
  <data name="Waitlisted" xml:space="preserve">
    <value>Beklemeye alındı</value>
  </data>
  <data name="QueueStatus" xml:space="preserve">
    <value>{0} ücretsiz paket kuyrukta. {1}/{2} etkinleştirme kullanıldı.</value>
    <comment>{0} will be replaced by a number, {1} will be replaced by a number, {2} will be replaced by a number</comment>
  </data>
  <data name="QueuePausedWhileIngame" xml:space="preserve">
    <value>Etkinleştirmeler, hesap oyun oynarken duraklatıldı.</value>
  </data>
  <data name="QueueLimitedUntil" xml:space="preserve">
    <value>Etkinleştirmeler {0} tarihinde yeniden başlayacak.</value>
    <comment>{0} will be replaced by a time</comment>
  </data>
  <data name="QueueDiscoveryStatus" xml:space="preserve">
    <value>{0} uygulama ve {1} paket keşfedildi ancak henüz işlenmedi.</value>
    <comment>{0} will be replaced by a number, {1} will be replaced by a number</comment>
  </data>
  <data name="PICSRestart" xml:space="preserve">
    <value>PICS yeniden başlatıldı, değişiklik numarası {0}'den {1}'e atlanıyor</value>
    <comment>{0} will be replaced by a number, {1} will be replaced by a number</comment>
  </data>
  <data name="MissedApps" xml:space="preserve">
    <value>PICS yeniden başlatması nedeniyle bazı ücretsiz uygulamalar kaçırılmış olabilir</value>
  </data>
  <data name="RecoveredApps" xml:space="preserve">
    <value>{0} uygulama değişikliği, değişiklik numarası {1}'de kurtarıldı</value>
    <comment>{0} will be replaced by a number, {1} will be replaced by a number</comment>
  </data>
  <data name="MissedPackages" xml:space="preserve">
    <value>PICS yeniden başlatması nedeniyle bazı ücretsiz paketler kaçırılmış olabilir</value>
  </data>
  <data name="RecoveredPackages" xml:space="preserve">
    <value>{0} paket değişikliği, değişiklik numarası {1}'de kurtarıldı</value>
    <comment>{0} will be replaced by a number, {1} will be replaced by a number</comment>
  </data>
  <data name="NoPackagesFound" xml:space="preserve">
    <value>Paket bulunamadı</value>
  </data>
  <data name="NoAppsFound" xml:space="preserve">
    <value>Uygulama bulunamadı</value>
  </data>
  <data name="AppListFetchFailed" xml:space="preserve">
    <value>Uygulama listesi alınamadı</value>
  </data>
  <data name="PluginNotEnabled" xml:space="preserve">
    <value>Ücretsiz Paketler eklentisi etkin değil</value>
  </data>
  <data name="ASFInfoParseFailed" xml:space="preserve">
    <value>ASFInfo'dan gelen veriler çözümlenemedi</value>
  </data>
  <data name="RemovingPackages" xml:space="preserve">
    <value>{0} paket kaldırılıyor.</value>
    <comment>{0} will be replaced by a number</comment>
  </data>
  <data name="LicensePageFetchFail" xml:space="preserve">
    <value>Lisanslar sayfası alınamadı</value>
    <comment>
</comment>
  </data>
  <data name="LicensePageEmpty" xml:space="preserve">
    <value>Kaldırılabilir paket bulunamadı</value>
    <comment>
</comment>
  </data>
  <data name="RemovingNoPackages" xml:space="preserve">
    <value>Kaldırılacak ücretsiz paket bulunamadı</value>
    <comment>
</comment>
  </data>
  <data name="ProductInfoFetchFailed" xml:space="preserve">
    <value>Ürün bilgileri alınamadı</value>
    <comment>
</comment>
  </data>
  <data name="RemovalQueueEmpty" xml:space="preserve">
    <value>Kaldırılan paket yok</value>
  </data>
  <data name="RemovalsCancelled" xml:space="preserve">
    <value>{0} kaldırma işlemi iptal edildi.</value>
    <comment>{0} will be replaced by a number</comment>
  </data>
  <data name="RemovalWaitMessage" xml:space="preserve">
    <value>Kaldırılacak ücretsiz paketler aranıyor, bu tarama ~{0} dakika sürecek. Herhangi bir zamanda şu komutla iptal edin: {1}</value>
    <comment>{0} will be replaced by a number, {1} will be replaced with a command</comment>
  </data>
  <data name="RemovablePackagesFound" xml:space="preserve">
    <value>Kaldırılacak {0} paket bulundu: 
{1}
İptal etmek için: {2}
Kaldırmaya devam etmek için: {3}
Devam etmeden önce listedeki öğeleri kaldırmak için: {4}</value>
    <comment>{0} will be replaced by a number, {1} will be replaced by a list, {2} will be replaced with a command, {3} will be replaced with a command, {4} will be replaced with a command</comment>
  </data>
  <data name="RemovingNoUnwatedPackages" xml:space="preserve">
    <value>Kaldırılacak istenmeyen ücretsiz paket bulunamadı</value>
    <comment>
</comment>
  </data>
  <data name="RemovalScanCancelled" xml:space="preserve">
    <value>İstenmeyen ücretsiz paketler için tarama iptal edildi.</value>
    <comment>
</comment>
  </data>
  <data name="RemovalScanAlreadyRunning" xml:space="preserve">
    <value>Zaten istenmeyen ücretsiz paketler taranıyor</value>
    <comment>
</comment>
  </data>
  <data name="RemovalPackageNotFound" xml:space="preserve">
    <value>{0} taranan paketler listesinde yoktu</value>
    <comment>{0} will be replaced by a packageID</comment>
  </data>
  <data name="RemovalPackageCancelled" xml:space="preserve">
    <value>{0} kaldırıldı</value>
    <comment>{0} will be replaced by a packageID</comment>
  </data>
  <data name="RemovalScanCanceling" xml:space="preserve">
    <value>İstenmeyen ücretsiz paketler için tarama iptal ediliyor...</value>
    <comment>
</comment>
  </data>
  <data name="RemovalScanNeeded" xml:space="preserve">
    <value>Önce şu komutla ücretsiz paketleri taramanız gerekiyor: {0}</value>
    <comment>{0} will be replaced by a command</comment>
  </data>
  <data name="RemovalsPaused" xml:space="preserve">
    <value>Paket kaldırma işlemleri {0} tarihine kadar duraklatıldı</value>
    <comment>{0} will be replaced by a time</comment>
  </data>
  <data name="PlaytimeFetchFailed" xml:space="preserve">
    <value>Oynama süresi bilgisi alınamadı</value>
    <comment>
</comment>
  </data>
</root>


================================================
FILE: FreePackages/Localization/Strings.uk-UA.resx
================================================
<?xml version="1.0" encoding="utf-8"?>
<root>
  <!--
    Microsoft ResX Schema

    Version 2.0

    The primary goals of this format is to allow a simple XML format
    that is mostly human readable. The generation and parsing of the
    various data types are done through the TypeConverter classes
    associated with the data types.

    Example:

    ... ado.net/XML headers & schema ...
    <resheader name="resmimetype">text/microsoft-resx</resheader>
    <resheader name="version">2.0</resheader>
    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
        <value>[base64 mime encoded serialized .NET Framework object]</value>
    </data>
    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
        <comment>This is a comment</comment>
    </data>

    There are any number of "resheader" rows that contain simple
    name/value pairs.

    Each data row contains a name, and value. The row also contains a
    type or mimetype. Type corresponds to a .NET class that support
    text/value conversion through the TypeConverter architecture.
    Classes that don't support this are serialized and stored with the
    mimetype set.

    The mimetype is used for serialized objects, and tells the
    ResXResourceReader how to depersist the object. This is currently not
    extensible. For a given mimetype the value must be set accordingly:

    Note - application/x-microsoft.net.object.binary.base64 is the format
    that the ResXResourceWriter will generate, however the reader can
    read any of the formats listed below.

    mimetype: application/x-microsoft.net.object.binary.base64
    value   : The object must be serialized with
            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
            : and then encoded with base64 encoding.

    mimetype: application/x-microsoft.net.object.soap.base64
    value   : The object must be serialized with
            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
            : and then encoded with base64 encoding.

    mimetype: application/x-microsoft.net.object.bytearray.base64
    value   : The object must be serialized into a byte array
            : using a System.ComponentModel.TypeConverter
            : and then encoded with base64 encoding.
    -->
  <xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
    <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
    <xsd:element name="root" msdata:IsDataSet="true">
      <xsd:complexType>
        <xsd:choice maxOccurs="unbounded">
          <xsd:element name="metadata">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0"/>
              </xsd:sequence>
              <xsd:attribute name="name" use="required" type="xsd:string"/>
              <xsd:attribute name="type" type="xsd:string"/>
              <xsd:attribute name="mimetype" type="xsd:string"/>
              <xsd:attribute ref="xml:space"/>
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="assembly">
            <xsd:complexType>
              <xsd:attribute name="alias" type="xsd:string"/>
              <xsd:attribute name="name" type="xsd:string"/>
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="data">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
              <xsd:attribute ref="xml:space"/>
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="resheader">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required"/>
            </xsd:complexType>
          </xsd:element>
        </xsd:choice>
      </xsd:complexType>
    </xsd:element>
  </xsd:schema>
  <resheader name="resmimetype">
    <value>text/microsoft-resx</value>
  </resheader>
  <resheader name="version">
    <value>2.0</value>
  </resheader>
  <resheader name="reader">
    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <resheader name="writer">
    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <data name="BadgeDataFetchFailed" xml:space="preserve">
    <value>Не вдалося отримати дані о значках для безплатних пакетів</value>
    <comment/>
  </data>
  <data name="BadgeDataParsingFailed" xml:space="preserve">
    <value>Не вдалося зібрати дані о значках для безплатних пакетів</value>
    <comment/>
  </data>
  <data name="PlaytestConfigLimitTriggered" xml:space="preserve">
    <value>Значення фільтра PlaytestMode було автоматично замінено на "0" (ігнорувати все), оскільки його може використовувати тільки один бот</value>
    <comment/>
  </data>
  <data name="QueueEmpty" xml:space="preserve">
    <value>Черга порожня</value>
    <comment/>
  </data>
  <data name="PackagesRemoved" xml:space="preserve">
    <value>Безплатних пакетів видалено: {0}.</value>
    <comment>{0} will be replaced by a number</comment>
  </data>
  <data name="DiscoveredAppsRemoved" xml:space="preserve">
    <value>Виявлених застосунків видалено: {0}.</value>
    <comment>{0} will be replaced by a number</comment>
  </data>
  <data name="DiscoveredPackagesRemoved" xml:space="preserve">
    <value>Виявлених пакетів видалено: {0}.</value>
    <comment>{0} will be replaced by a number</comment>
  </data>
  <data name="DiscoveredAppsAdded" xml:space="preserve">
    <value>Додано {0} до черги виявлених застосунків</value>
    <comment>{0} will be replaced by an appID</comment>
  </data>
  <data name="DiscoveredPackagesAdded" xml:space="preserve">
    <value>Додано {0} до черги виявлених пакетів</value>
    <comment>{0} will be replaced by a subID</comment>
  </data>
  <data name="AppsQueued" xml:space="preserve">
    <value>Додано {0} до черги безплатних пакетів</value>
    <comment>{0} will be replaced by an appID</comment>
  </data>
  <data name="PackagesQueued" xml:space="preserve">
    <value>Додано {0} до черги безплатних пакетів</value>
    <comment>{0} will be replaced by a subID</comment>
  </data>
  <data name="ActivationPaused" xml:space="preserve">
    <value>Призупинення активації безплатних пакетів до {0}</value>
    <comment>{0} will be replaced by a time</comment>
  </data>
  <data name="RateLimitExceeded" xml:space="preserve">
    <value>Перевищено ліміт безплатних пакетів</value>
    <comment/>
  </data>
  <data name="ReplacedWith" xml:space="preserve">
    <value>Замінено на {0}</value>
    <comment>{0} will be replaced by a number</comment>
  </data>
  <data name="Unknown" xml:space="preserve">
    <value>Невідомо</value>
    <comment/>
  </data>
  <data name="Invalid" xml:space="preserve">
    <value>Некоректно</value>
    <comment/>
  </data>
  <data name="Failed" xml:space="preserve">
    <value>Невдало</value>
    <comment/>
  </data>
  <data name="Waitlisted" xml:space="preserve">
    <value>У списку очікування</value>
    <comment/>
  </data>
  <data name="QueueStatus" xml:space="preserve">
    <value>Безплатних пакетів у черзі: {0}. Активацій використано: {1}/{2}.</value>
    <comment>{0} will be replaced by a number, {1} will be replaced by a number, {2} will be replaced by a number</comment>
  </data>
  <data name="QueuePausedWhileIngame" xml:space="preserve">
    <value>Активації призупинені, оскільки обліковий запис використовується для гри.</value>
    <comment/>
  </data>
  <data name="QueueLimitedUntil" xml:space="preserve">
    <value>Активації поновляться в {0}.</value>
    <comment>{0} will be replaced by a time</comment>
  </data>
  <data name="QueueDiscoveryStatus" xml:space="preserve">
    <value>Виявлено {0} застосунків та {1} пакетів, але ще не оброблено.</value>
    <comment>{0} will be replaced by a number, {1} will be replaced by a number</comment>
  </data>
  <data name="PICSRestart" xml:space="preserve">
    <value>PICS перезапущено, перейшовши від номера зміни {0} до {1}</value>
    <comment>{0} will be replaced by a number, {1} will be replaced by a number</comment>
  </data>
  <data name="MissedApps" xml:space="preserve">
    <value>Можливо, пропущено деякі безплатні застосунки через перезапуск PICS</value>
    <comment/>
  </data>
  <data name="RecoveredApps" xml:space="preserve">
    <value>Відновлено {0} змін застосунку з номером зміни {1}</value>
    <comment>{0} will be replaced by a number, {1} will be replaced by a number</comment>
  </data>
  <data name="MissedPackages" xml:space="preserve">
    <value>Можливо, пропущено деякі безплатні пакети через перезапуск PICS</value>
    <comment/>
  </data>
  <data name="RecoveredPackages" xml:space="preserve">
    <value>Відновлено {0} змін пакетів з номером зміни {1}</value>
    <comment>{0} will be replaced by a number, {1} will be replaced by a number</comment>
  </data>
  <data name="NoPackagesFound" xml:space="preserve">
    <value>Пакети не знайдено</value>
    <comment/>
  </data>
  <data name="NoAppsFound" xml:space="preserve">
    <value>Застосунки не знайдено</value>
    <comment/>
  </data>
  <data name="AppListFetchFailed" xml:space="preserve">
    <value>Не вдалося отримати список застосунків</value>
    <comment/>
  </data>
  <data name="PluginNotEnabled" xml:space="preserve">
    <value>Плагін Free Packages не ввімкнено</value>
    <comment/>
  </data>
  <data name="ASFInfoParseFailed" xml:space="preserve">
    <value>Не вдалося обробити дані з ASFInfo</value>
    <comment/>
  </data>
</root>

================================================
FILE: FreePackages/Localization/Strings.zh-Hans.resx
================================================
<?xml version="1.0" encoding="utf-8"?>
<root>
  <!--
    Microsoft ResX Schema

    Version 2.0

    The primary goals of this format is to allow a simple XML format
    that is mostly human readable. The generation and parsing of the
    various data types are done through the TypeConverter classes
    associated with the data types.

    Example:

    ... ado.net/XML headers & schema ...
    <resheader name="resmimetype">text/microsoft-resx</resheader>
    <resheader name="version">2.0</resheader>
    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
        <value>[base64 mime encoded serialized .NET Framework object]</value>
    </data>
    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
        <comment>This is a comment</comment>
    </data>

    There are any number of "resheader" rows that contain simple
    name/value pairs.

    Each data row contains a name, and value. The row also contains a
    type or mimetype. Type corresponds to a .NET class that support
    text/value conversion through the TypeConverter architecture.
    Classes that don't support this are serialized and stored with the
    mimetype set.

    The mimetype is used for serialized objects, and tells the
    ResXResourceReader how to depersist the object. This is currently not
    extensible. For a given mimetype the value must be set accordingly:

    Note - application/x-microsoft.net.object.binary.base64 is the format
    that the ResXResourceWriter will generate, however the reader can
    read any of the formats listed below.

    mimetype: application/x-microsoft.net.object.binary.base64
    value   : The object must be serialized with
            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
            : and then encoded with base64 encoding.

    mimetype: application/x-microsoft.net.object.soap.base64
    value   : The object must be serialized with
            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
            : and then encoded with base64 encoding.

    mimetype: application/x-microsoft.net.object.bytearray.base64
    value   : The object must be serialized into a byte array
            : using a System.ComponentModel.TypeConverter
            : and then encoded with base64 encoding.
    -->
  <xsd:schema xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata" id="root">
    <xsd:import namespace="http://www.w3.org/XML/1998/namespace"/>
    <xsd:element name="root" msdata:IsDataSet="true">
      <xsd:complexType>
        <xsd:choice maxOccurs="unbounded">
          <xsd:element name="metadata">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0"/>
              </xsd:sequence>
              <xsd:attribute name="name" use="required" type="xsd:string"/>
              <xsd:attribute name="type" type="xsd:string"/>
              <xsd:attribute name="mimetype" type="xsd:string"/>
              <xsd:attribute ref="xml:space"/>
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="assembly">
            <xsd:complexType>
              <xsd:attribute name="alias" type="xsd:string"/>
              <xsd:attribute name="name" type="xsd:string"/>
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="data">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2"/>
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1"/>
              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3"/>
              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4"/>
              <xsd:attribute ref="xml:space"/>
            </xsd:complexType>
          </xsd:element>
          <xsd:element name="resheader">
            <xsd:complexType>
              <xsd:sequence>
                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1"/>
              </xsd:sequence>
              <xsd:attribute name="name" type="xsd:string" use="required"/>
            </xsd:complexType>
          </xsd:element>
        </xsd:choice>
      </xsd:complexType>
    </xsd:element>
  </xsd:schema>
  <resheader name="resmimetype">
    <value>text/microsoft-resx</value>
  </resheader>
  <resheader name="version">
    <value>2.0</value>
  </resheader>
  <resheader name="reader">
    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <resheader name="writer">
    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
  </resheader>
  <data name="BadgeDataFetchFailed" xml:space="preserve">
    <value>获取免费包徽章数据失败</value>
    <comment/>
  </data>
  <data name="BadgeDataParsingFailed" xml:space="preserve">
    <value>解析免费包徽章数据失败</value>
    <comment/>
  </data>
  <data name="PlaytestConfigLimitTriggered" xml:space="preserve">
    <value>已将 PlaytestMode 更改为 0 (None),只有一个机器人可使用此过滤器</value>
    <comment/>
  </data>
  <data name="QueueEmpty" xml:space="preserve">
    <value>队列为空</value>
    <comment/>
  </data>
  <data name="PackagesRemoved" xml:space="preserve">
    <value>{0} 免费包已移除。</value>
    <comment>{0} will be replaced by a number</comment>
  </data>
  <data name="DiscoveredAppsRemoved" xml:space="preserve">
    <value>{0} 已发现应用已移除。</value>
    <comment>{0} will be replaced by a number</comment>
  </data>
  <data name="DiscoveredPackagesRemoved" xml:space="preserve">
    <value>{0} 已发现包已移除。</value>
    <comment>{0} will be replaced by a number</comment>
  </data>
  <data name="DiscoveredAppsAdded" xml:space="preserve">
    <value>添加 {0} 至已发现应用队列</value>
    <comment>{0} will be replaced by an appID</comment>
  </data>
Download .txt
gitextract_ow9uday2/

├── .editorconfig
├── .gitattributes
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.md
│   │   ├── feature_request.md
│   │   └── question.md
│   ├── RELEASE_TEMPLATE.md
│   └── workflows/
│       └── publish.yml
├── .gitignore
├── .gitmodules
├── Directory.Build.props
├── Directory.Packages.props
├── FreePackages/
│   ├── .gitignore
│   ├── Commands.cs
│   ├── Data/
│   │   ├── Cache/
│   │   │   ├── BotCache.cs
│   │   │   └── GlobalCache.cs
│   │   ├── External/
│   │   │   ├── ASFInfo.cs
│   │   │   └── CardApps.cs
│   │   └── PICS/
│   │       └── ProductInfo.cs
│   ├── FreePackages.cs
│   ├── FreePackages.csproj
│   ├── Handlers/
│   │   ├── PICSHandler.cs
│   │   ├── PackageHandler.cs
│   │   └── SteamHandler.cs
│   ├── Helpers/
│   │   ├── DeterministicHasher.cs
│   │   └── StatusReporter.cs
│   ├── IPC/
│   │   ├── Api/
│   │   │   └── FreePackagesController.cs
│   │   ├── Requests/
│   │   │   └── QueueLicensesRequest.cs
│   │   └── Responses/
│   │       └── FreeSubResponse.cs
│   ├── Json.cs
│   ├── Localization/
│   │   ├── README.md
│   │   ├── Strings.de-DE.resx
│   │   ├── Strings.resx
│   │   ├── Strings.ru-RU.resx
│   │   ├── Strings.tr-TR.resx
│   │   ├── Strings.uk-UA.resx
│   │   ├── Strings.zh-Hans.resx
│   │   └── Strings.zh-Hant.resx
│   ├── PackageFilter/
│   │   ├── FilterConfig.cs
│   │   ├── Filterables/
│   │   │   ├── FilterableApp.cs
│   │   │   └── FilterablePackage.cs
│   │   └── PackageFilter.cs
│   ├── PackageQueue/
│   │   ├── ActivationQueue.cs
│   │   ├── Package.cs
│   │   ├── PackageQueue.cs
│   │   └── RemovalQueue.cs
│   └── WebRequest.cs
├── FreePackages.Tests/
│   ├── Apps.cs
│   ├── Filters.cs
│   ├── FreePackages.Tests.csproj
│   ├── Packages.cs
│   ├── TestData/
│   │   ├── app_which_is_free.txt
│   │   ├── app_with_categories.txt
│   │   ├── app_with_content_descriptors.txt
│   │   ├── app_with_deck_playable.txt
│   │   ├── app_with_deck_unknown.txt
│   │   ├── app_with_deck_unsupported.txt
│   │   ├── app_with_deck_verified.txt
│   │   ├── app_with_dlc.txt
│   │   ├── app_with_language_support.txt
│   │   ├── app_with_purchase_restricted_countries.txt
│   │   ├── app_with_release_state.txt
│   │   ├── app_with_required_app.txt
│   │   ├── app_with_restricted_countries.txt
│   │   ├── app_with_review_score.txt
│   │   ├── app_with_state.txt
│   │   ├── app_with_tags.txt
│   │   ├── app_with_type.txt
│   │   ├── demo_which_will_be_removed.txt
│   │   ├── demo_with_fewer_categories_than_parent.txt
│   │   ├── demo_with_fewer_categories_than_parent_parent.txt
│   │   ├── demo_with_fewer_content_descriptors_than_parent.txt
│   │   ├── demo_with_fewer_content_descriptors_than_parent_parent.txt
│   │   ├── demo_with_fewer_languages_than_parent.txt
│   │   ├── demo_with_fewer_languages_than_parent_parent.txt
│   │   ├── demo_with_fewer_tags_than_parent.txt
│   │   ├── demo_with_fewer_tags_than_parent_parent.txt
│   │   ├── package_which_is_free.txt
│   │   ├── package_which_is_no_cost.txt
│   │   ├── package_with_deactivated_demo.txt
│   │   ├── package_with_demo_which_will_be_removed.txt
│   │   ├── package_with_disallowed_app.txt
│   │   ├── package_with_free_weekend.txt
│   │   ├── package_with_purchase_restricted_countries.txt
│   │   ├── package_with_restricted_countries.txt
│   │   ├── package_with_single_app.txt
│   │   ├── package_with_single_app_app_1.txt
│   │   ├── package_with_timed_activation.txt
│   │   ├── playtest_with_hidden_parent.txt
│   │   ├── playtest_with_hidden_parent_parent.txt
│   │   ├── playtest_with_no_categories.txt
│   │   ├── playtest_with_no_categories_parent.txt
│   │   ├── playtest_with_no_languages.txt
│   │   ├── playtest_with_no_languages_parent.txt
│   │   ├── playtest_with_no_waitlist.txt
│   │   ├── playtest_with_no_waitlist_parent.txt
│   │   ├── userdata_empty.json
│   │   ├── userdata_with_excluded_content_descriptors.json
│   │   ├── userdata_with_excluded_tags.json
│   │   ├── userdata_with_followed_apps.json
│   │   ├── userdata_with_ignored_apps.json
│   │   ├── userdata_with_wishlist_apps.json
│   │   └── userinfo_empty.json
│   └── generate_test_data.sh
├── FreePackages.sln
├── FreePackagesImporter/
│   ├── README.md
│   └── code.user.js
├── LICENSE
├── README.md
├── SECURITY.md
├── build.bat
├── build.sh
├── crowdin.yml
└── github-pandoc.css
Download .txt
SYMBOL INDEX (300 symbols across 29 files)

FILE: FreePackages.Tests/Apps.cs
  class Apps (line 10) | [TestClass]
    method InitializePackageFilter (line 15) | [TestInitialize]
    method CleanupPackageFilter (line 22) | [TestCleanup]
    method CanDetectFreeApp (line 28) | [TestMethod]
    method CanDetectAvailableAppByReleaseState (line 35) | [TestMethod]
    method CanDetectAvailableAppByState (line 43) | [TestMethod]
    method CanDetectRedeemableAppWithAppRequirement (line 51) | [TestMethod]
    method CanDetectRedeemableAppWithRestrictedCountry (line 62) | [TestMethod]
    method CanDetectRedeemableAppWithPurchaseRestrictedCountry (line 74) | [TestMethod]
    method CanFindAppDLC (line 87) | [TestMethod]
    method CanDetectNonRedeemablePlaytestWithHiddenParent (line 95) | [TestMethod]

FILE: FreePackages.Tests/Filters.cs
  class Filters (line 9) | [TestClass]
    method InitializePackageFilter (line 15) | [TestInitialize]
    method CleanupPackageFilter (line 23) | [TestCleanup]
    method CanFilterAppByType (line 30) | [TestMethod]
    method CanFilterAppByTag (line 47) | [TestMethod]
    method CanFilterAppAppByParentTag (line 73) | [TestMethod]
    method CanFilterAppByCategory (line 97) | [TestMethod]
    method CanFilterAppWithNoCategoryByParentCategory (line 123) | [TestMethod]
    method CanFilterAppByParentCategory (line 146) | [TestMethod]
    method CanFilterAppByLanguage (line 161) | [TestMethod]
    method CanFilterAppWithNoLanguageByParentLanguage (line 174) | [TestMethod]
    method CanFilterAppByParentLanguage (line 188) | [TestMethod]
    method CanFilterAppByReviewScore (line 203) | [TestMethod]
    method CanFilterAppByContentDescriptor (line 216) | [TestMethod]
    method CanFilterAppAppByParentContentDescriptor (line 227) | [TestMethod]
    method CanFilterAppByID (line 241) | [TestMethod]
    method CanFilterAppByParentID (line 252) | [TestMethod]
    method CanFilterAppByPlaytest (line 266) | [TestMethod]
    method CanFilterPackageByFreeWeekend (line 278) | [TestMethod]
    method CanFilterPackageByContents (line 289) | [TestMethod]
    method CanFilterByStoreData (line 306) | [TestMethod]
    method CanUseMultipleFilters (line 327) | [TestMethod]
    method CanFilterAppBySystem (line 350) | [TestMethod]
    method CanFilterPackageByNoCost (line 388) | [TestMethod]
    method CanFilterByWishlist (line 402) | [TestMethod]
    method CanFilterByReleaseDate (line 421) | [TestMethod]
    method CanFilterDemos (line 438) | [TestMethod]

FILE: FreePackages.Tests/Packages.cs
  class Packages (line 10) | [DeploymentItem("TestData")]
    method InitializePackageFilter (line 18) | [TestInitialize]
    method CleanupPackageFilter (line 53) | [TestCleanup]
    method CanDetectFreePackage (line 71) | [TestMethod]
    method CanDetectPackageDemoState (line 84) | [TestMethod]
    method CanDetectPackageTimeRestrictions (line 97) | [TestMethod]
    method CanDetectPackageDisallowedApp (line 111) | [TestMethod]
    method CanDetectPackageRestrictedCountry (line 124) | [TestMethod]
    method CanDetectPackagePurchaseRestrictedCountry (line 139) | [TestMethod]
    method Dispose (line 154) | public void Dispose() => BotCache?.Dispose();

FILE: FreePackages/Commands.cs
  class Commands (line 16) | internal static class Commands {
    method Response (line 17) | internal static async Task<string?> Response(Bot bot, EAccess access, ...
    method ResponseCancelRemove (line 108) | private static string? ResponseCancelRemove(Bot bot, EAccess access) {
    method ResponseCancelRemove (line 124) | private static string? ResponseCancelRemove(EAccess access, ulong stea...
    method ResponseConfirmRemove (line 142) | private static string? ResponseConfirmRemove(Bot bot, EAccess access) {
    method ResponseConfirmRemove (line 158) | private static string? ResponseConfirmRemove(EAccess access, ulong ste...
    method ResponseClearQueue (line 176) | private static string? ResponseClearQueue(Bot bot, EAccess access) {
    method ResponseClearQueue (line 192) | private static string? ResponseClearQueue(EAccess access, ulong steamI...
    method ResponseDontRemove (line 210) | private static string? ResponseDontRemove(Bot bot, EAccess access, str...
    method ResponseDontRemove (line 264) | private static string? ResponseDontRemove(EAccess access, ulong steamI...
    method ResponseQueueStatus (line 284) | private static string? ResponseQueueStatus(Bot bot, EAccess access) {
    method ResponseQueueStatus (line 300) | private static string? ResponseQueueStatus(EAccess access, ulong steam...
    method ResponseQueueLicense (line 318) | private static string? ResponseQueueLicense(Bot bot, EAccess access, s...
    method ResponseQueueLicense (line 376) | private static string? ResponseQueueLicense(EAccess access, ulong stea...
    method ResponseRemoveFreePackages (line 396) | private static async Task<string?> ResponseRemoveFreePackages(Bot bot,...
    method ResponseRemoveFreePackages (line 450) | private static async Task<string?> ResponseRemoveFreePackages(EAccess ...
    method FormatStaticResponse (line 464) | internal static string FormatStaticResponse(string response) => ArchiS...
    method FormatBotResponse (line 465) | internal static string FormatBotResponse(Bot bot, string response) => ...

FILE: FreePackages/Data/Cache/BotCache.cs
  class BotCache (line 14) | internal sealed class BotCache : SerializableFile {
    method BotCache (line 50) | [JsonConstructor]
    method BotCache (line 53) | internal BotCache(string filePath) : this() {
    method Save (line 61) | protected override Task Save() => Save(this);
    method CreateOrLoad (line 63) | internal static async Task<BotCache?> CreateOrLoad(string filePath) {
    method AddPackage (line 101) | internal bool AddPackage(Package package) {
    method AddPackages (line 112) | internal bool AddPackages(IEnumerable<Package> packages) {
    method RemovePackage (line 124) | internal bool RemovePackage(Package package) {
    method RemoveAppPackages (line 131) | internal bool RemoveAppPackages(HashSet<uint> appIDsToRemove) {
    method GetNextPackage (line 138) | internal Package? GetNextPackage(HashSet<EPackageType> types) {
    method AddActivation (line 149) | internal void AddActivation(DateTime activation, uint count = 1, IRead...
    method NumActivationsPastPeriod (line 171) | internal int NumActivationsPastPeriod() {
    method GetLastActivation (line 175) | internal DateTime? GetLastActivation() {
    method AddChanges (line 187) | internal void AddChanges(HashSet<uint>? appIDs = null, HashSet<uint>? ...
    method RemoveChange (line 207) | internal void RemoveChange(uint? appID = null, uint? packageID = null,...
    method SaveChanges (line 221) | internal void SaveChanges() {
    method ClearQueue (line 225) | internal void ClearQueue() {
    method CancelRemoval (line 232) | internal void CancelRemoval() {
    method AddWaitlistedPlaytest (line 237) | internal void AddWaitlistedPlaytest(uint appID) {
    method UpdateSeenPackages (line 243) | internal void UpdateSeenPackages(List<SteamApps.LicenseListCallback.Li...
    method IgnoreApp (line 264) | internal void IgnoreApp(uint appID) {

FILE: FreePackages/Data/Cache/GlobalCache.cs
  class GlobalCache (line 10) | internal sealed class GlobalCache : SerializableFile {
    method ShouldSerializeLastChangeNumber (line 19) | public bool ShouldSerializeLastChangeNumber() => LastChangeNumber > 0;
    method ShouldSerializeLastASFInfoItemCount (line 20) | public bool ShouldSerializeLastASFInfoItemCount() => LastASFInfoItemCo...
    method GlobalCache (line 22) | [JsonConstructor]
    method Save (line 27) | protected override Task Save() => Save(this);
    method CreateOrLoad (line 29) | internal static async Task<GlobalCache> CreateOrLoad() {
    method UpdateChangeNumber (line 60) | internal void UpdateChangeNumber(uint currentChangeNumber) {
    method UpdateASFInfoItemCount (line 66) | internal void UpdateASFInfoItemCount(uint currentASFInfoItemCount) {

FILE: FreePackages/Data/External/ASFInfo.cs
  class ASFInfo (line 17) | internal static class ASFInfo {
    method Update (line 24) | internal static void Update() {
    method DoUpdate (line 28) | private static async Task DoUpdate() {

FILE: FreePackages/Data/External/CardApps.cs
  class CardApps (line 13) | internal static class CardApps {
    method Update (line 19) | internal static void Update() {
    method DoUpdate (line 23) | private static async Task DoUpdate() {
    class Badges (line 48) | private sealed class Badges {

FILE: FreePackages/Data/PICS/ProductInfo.cs
  class ProductInfo (line 11) | internal static class ProductInfo {
    method GetProductInfo (line 16) | internal async static Task<List<SteamApps.PICSProductInfoCallback>?> G...
    method GetProductIDBatches (line 33) | internal static IEnumerable<(HashSet<uint>?, HashSet<uint>?)> GetProdu...
    method FetchProductInfo (line 55) | private async static Task<List<SteamApps.PICSProductInfoCallback>?> Fe...

FILE: FreePackages/FreePackages.cs
  class FreePackages (line 13) | [Export(typeof(IPlugin))]
    method OnLoaded (line 20) | public Task OnLoaded() {
    method OnBotCommand (line 26) | public async Task<string?> OnBotCommand(Bot bot, EAccess access, strin...
    method OnASFInit (line 30) | public async Task OnASFInit(IReadOnlyDictionary<string, JsonElement>? ...
    method OnBotInitModules (line 39) | public async Task OnBotInitModules(Bot bot, IReadOnlyDictionary<string...
    method GetPreferredChangeNumberToStartFrom (line 94) | public Task<uint> GetPreferredChangeNumberToStartFrom() {
    method OnPICSChanges (line 98) | public Task OnPICSChanges(uint currentChangeNumber, IReadOnlyDictionar...
    method OnPICSChangesRestart (line 104) | public async Task OnPICSChangesRestart(uint currentChangeNumber) {
    method OnBotSteamCallbacksInit (line 108) | public Task OnBotSteamCallbacksInit(Bot bot, CallbackManager callbackM...
    method OnBotSteamHandlersInit (line 114) | public Task<IReadOnlyCollection<ClientMsgHandler>?> OnBotSteamHandlers...
    method OnLicenseList (line 118) | private static void OnLicenseList (Bot bot, SteamApps.LicenseListCallb...
    method OnBotLoggedOn (line 122) | public async Task OnBotLoggedOn(Bot bot) {
    method OnBotDisconnected (line 126) | public Task OnBotDisconnected(Bot bot, EResult reason) {

FILE: FreePackages/Handlers/PICSHandler.cs
  class PICSHandler (line 12) | internal static class PICSHandler {
    method OnPICSChanges (line 16) | internal static void OnPICSChanges(uint currentChangeNumber, IReadOnly...
    method OnPICSRestart (line 31) | internal async static Task OnPICSRestart(uint currentChangeNumber) {
    method FindOldestPICSChanges (line 82) | private async static Task<SteamApps.PICSChangesCallback?> FindOldestPI...
    method FetchPICSChanges (line 112) | private async static Task<SteamApps.PICSChangesCallback?> FetchPICSCha...
    method GetRefreshBot (line 135) | private static Bot? GetRefreshBot() => Bot.BotsReadOnly?.Values.FirstO...

FILE: FreePackages/Handlers/PackageHandler.cs
  class PackageHandler (line 14) | internal sealed class PackageHandler : IDisposable {
    method PackageHandler (line 28) | private PackageHandler(Bot bot, BotCache botCache, List<FilterConfig> ...
    method Dispose (line 37) | public void Dispose() {
    method AddHandler (line 42) | internal static async Task AddHandler(Bot bot, List<FilterConfig> filt...
    method OnLicenseList (line 72) | internal static void OnLicenseList(Bot bot, SteamApps.LicenseListCallb...
    method OnBotLoggedOn (line 80) | internal static async Task OnBotLoggedOn(Bot bot) {
    method UpdateUserData (line 90) | private void UpdateUserData() {
    method FetchUserData (line 94) | private async Task FetchUserData() {
    method AddChanges (line 122) | internal static void AddChanges(IReadOnlyDictionary<uint, SteamApps.PI...
    method IsReady (line 137) | private async static Task<bool> IsReady(uint maxWaitTimeSeconds = 120) {
    method HandleChanges (line 151) | internal async static Task HandleChanges() {
    method HandleProductInfo (line 181) | private async static Task HandleProductInfo(List<SteamApps.PICSProduct...
    method HandleFreeApp (line 252) | private void HandleFreeApp(FilterableApp app) {
    method HandleFreePackage (line 276) | private void HandleFreePackage(FilterablePackage package) {
    method HandlePlaytest (line 305) | private void HandlePlaytest(FilterableApp app) {
    method HandleNewPackage (line 333) | private void HandleNewPackage(FilterablePackage package) {
    method HandleLicenseList (line 369) | internal void HandleLicenseList(SteamApps.LicenseListCallback callback) {
    method GetStatus (line 390) | internal string GetStatus() {
    method ClearQueue (line 421) | internal string ClearQueue() {
    method AddPackage (line 447) | internal string AddPackage(EPackageType type, uint id, bool useFilter) {
    method AddPackages (line 469) | internal void AddPackages(HashSet<uint>? appIDs, HashSet<uint>? packag...
    method ScanRemovables (line 487) | internal async Task ScanRemovables(Dictionary<uint, string> removeable...
    method ConfirmRemoval (line 598) | internal string ConfirmRemoval() {
    method ModifyRemovables (line 610) | internal string ModifyRemovables(EPackageType type, uint id) {
    method CancelRemoval (line 633) | internal string CancelRemoval() {

FILE: FreePackages/Handlers/SteamHandler.cs
  class SteamHandler (line 12) | internal sealed class SteamHandler : ClientMsgHandler {
    method AddHandler (line 15) | internal static SteamHandler AddHandler(Bot bot) {
    method HandleMsg (line 26) | public override void HandleMsg(IPacketMsg packetMsg) { }
    method GetOwnedGames (line 28) | public async Task<Dictionary<uint, CPlayer_GetOwnedGames_Response.Game...

FILE: FreePackages/Helpers/DeterministicHasher.cs
  class DeterministicHasher (line 6) | internal static class DeterministicHasher {
    method Hash (line 10) | internal static int Hash(int value) => Hash(FnvOffsetBias, value);
    method Hash (line 11) | internal static int Hash(uint value) => Hash(FnvOffsetBias, value);
    method Hash (line 12) | internal static int Hash(bool value) => Hash(FnvOffsetBias, value);
    method Hash (line 13) | internal static int Hash(string? str) => Hash(FnvOffsetBias, str);
    method Hash (line 14) | internal static int Hash(IEnumerable<string>? collection) => Hash(FnvO...
    method Hash (line 15) | internal static int Hash(IEnumerable<uint>? collection) => Hash(FnvOff...
    method Hash (line 16) | internal static int Hash(IEnumerable<FilterConfig>? collection) => Has...
    method Hash (line 18) | internal static int Hash(int hash, int value) => unchecked((hash ^ val...
    method Hash (line 19) | internal static int Hash(int hash, uint value) => Hash(hash, (int) val...
    method Hash (line 20) | internal static int Hash(int hash, bool value) => Hash(hash, value ? 1...
    method Hash (line 22) | internal static int Hash(int hash, string? str) {
    method Hash (line 34) | internal static int Hash(int hash, IEnumerable<string>? collection) {
    method Hash (line 46) | internal static int Hash(int hash, IEnumerable<uint>? collection) {
    method Hash (line 58) | internal static int Hash(int hash, IEnumerable<FilterConfig>? collecti...

FILE: FreePackages/Helpers/StatusReporter.cs
  class StatusReporter (line 16) | internal sealed class StatusReporter {
    method StatusReporter (line 35) | internal StatusReporter(Bot? sender = null, ulong recipientSteamID = 0...
    method StatusReporter (line 42) | [JsonConstructor]
    method StatusLogger (line 48) | internal static StatusReporter StatusLogger() {
    method Report (line 53) | internal void Report(Bot reportingBot, string report, bool suppressDup...
    method ForceSend (line 101) | internal void ForceSend() {
    method Send (line 105) | private async Task Send() {

FILE: FreePackages/IPC/Api/FreePackagesController.cs
  class FreePackagesController (line 18) | [Route("Api/FreePackages")]
    method GetChangesSince (line 20) | [HttpGet("{botNames:required}/GetChangesSince/{changeNumber:required}")]
    method GetProductInfo (line 51) | [HttpGet("{botNames:required}/GetProductInfo")]
    method RequestFreeAppLicense (line 107) | [HttpGet("{botName:required}/RequestFreeAppLicense")]
    method RequestFreeSubLicense (line 151) | [HttpGet("{botName:required}/RequestFreeSubLicense")]
    method GetOwnedPackages (line 183) | [HttpGet("{botName:required}/GetOwnedPackages")]
    method GetOwnedApps (line 204) | [HttpGet("{botName:required}/GetOwnedApps")]
    method QueueLicenses (line 256) | [Consumes("application/json")]

FILE: FreePackages/IPC/Requests/QueueLicensesRequest.cs
  class QueueLicensesRequest (line 5) | public sealed class QueueLicensesRequest {
    method QueueLicensesRequest (line 15) | [JsonConstructor]

FILE: FreePackages/IPC/Responses/FreeSubResponse.cs
  class FreeSubResponse (line 5) | public sealed class FreeSubResponse {
    method FreeSubResponse (line 14) | public FreeSubResponse(EResult result, EPurchaseResultDetail purchaseR...

FILE: FreePackages/Json.cs
  class Steam (line 7) | internal static class Steam {
    class PlaytestAccessResponse (line 8) | internal sealed class PlaytestAccessResponse {
      method PlaytestAccessResponse (line 19) | [JsonConstructor]
    class UserData (line 23) | internal sealed class UserData {
      method UserData (line 64) | [JsonConstructor]
    class Tag (line 68) | internal sealed class Tag {
      method Tag (line 84) | [JsonConstructor]
    class UserInfo (line 88) | internal sealed class UserInfo {
      method UserInfo (line 130) | [JsonConstructor]
    class EmptyArrayOrDictionaryConverter (line 135) | public class EmptyArrayOrDictionaryConverter : JsonConverter<Dictionar...
      method Read (line 136) | public override Dictionary<uint, uint> Read(ref Utf8JsonReader reade...
      method Write (line 154) | public override void Write(Utf8JsonWriter writer, Dictionary<uint, u...

FILE: FreePackages/PackageFilter/FilterConfig.cs
  class FilterConfig (line 6) | internal sealed class FilterConfig : IJsonOnDeserialized {
    method FilterConfig (line 67) | [JsonConstructor]
    method OnDeserialized (line 70) | public void OnDeserialized() {
    method GetHashCode (line 82) | public override int GetHashCode() {
  type EPlaytestMode (line 107) | [Flags]

FILE: FreePackages/PackageFilter/Filterables/FilterableApp.cs
  class FilterableApp (line 9) | internal sealed class FilterableApp {
    method FilterableApp (line 35) | internal FilterableApp(SteamApps.PICSProductInfoCallback.PICSProductIn...
    method FilterableApp (line 36) | internal FilterableApp(KeyValue kv) : this(kv["appid"].AsUnsignedInteg...
    method FilterableApp (line 37) | internal FilterableApp(uint id, KeyValue kv) {
    method GetFilterables (line 89) | internal static async Task<List<FilterableApp>?> GetFilterables(List<S...
    method AddParent (line 130) | internal void AddParent(SteamApps.PICSProductInfoCallback.PICSProductI...
    method AddParent (line 131) | internal void AddParent(KeyValue? kv) => AddParent(kv?["appid"].AsUnsi...
    method AddParent (line 132) | internal void AddParent(uint? id, KeyValue? kv) {
    method IsFree (line 140) | internal bool IsFree() {
    method IsAvailable (line 157) | internal bool IsAvailable() {
    method HasID (line 171) | internal bool HasID(IEnumerable<uint> ids) {
    method HasType (line 189) | internal bool HasType(IEnumerable<string> types) {
    method HasTag (line 198) | internal bool HasTag(IEnumerable<uint> tags, bool requireAll = false) {
    method HasCategory (line 220) | internal bool HasCategory(IEnumerable<uint> categories, bool requireAl...
    method HasContentDescriptor (line 245) | internal bool HasContentDescriptor(IEnumerable<uint> content_descripto...
    method HasLanguage (line 262) | internal bool HasLanguage(IEnumerable<string> languages) {
    method HasSystem (line 282) | internal bool HasSystem(IEnumerable<string> systems) {

FILE: FreePackages/PackageFilter/Filterables/FilterablePackage.cs
  class FilterablePackage (line 10) | internal sealed class FilterablePackage {
    method FilterablePackage (line 31) | internal FilterablePackage(SteamApps.PICSProductInfoCallback.PICSProdu...
    method FilterablePackage (line 32) | internal FilterablePackage(KeyValue kv) : this(Convert.ToUInt32(kv.Nam...
    method FilterablePackage (line 33) | internal FilterablePackage(uint id, KeyValue kv) {
    method GetFilterables (line 52) | internal static async Task<List<FilterablePackage>?> GetFilterables(Li...
    method AddPackageContents (line 119) | internal void AddPackageContents(IEnumerable<SteamApps.PICSProductInfo...
    method AddPackageContents (line 120) | internal void AddPackageContents(IEnumerable<KeyValue> kvs) => AddPack...
    method AddPackageContents (line 121) | internal void AddPackageContents(IEnumerable<(uint id, KeyValue kv)> p...
    method AddPackageContentParents (line 126) | internal void AddPackageContentParents(IEnumerable<SteamApps.PICSProdu...
    method AddPackageContentParents (line 127) | internal void AddPackageContentParents(IEnumerable<KeyValue> kvs) => A...
    method AddPackageContentParents (line 128) | internal void AddPackageContentParents(IEnumerable<(uint id, KeyValue ...
    method IsFree (line 141) | internal bool IsFree() {
    method IsAvailable (line 149) | internal bool IsAvailable() {
    method IsAvailablePackageContents (line 189) | internal bool IsAvailablePackageContents() {

FILE: FreePackages/PackageFilter/PackageFilter.cs
  class PackageFilter (line 9) | internal sealed class PackageFilter {
    method PackageFilter (line 21) | internal PackageFilter(BotCache botCache, List<FilterConfig> filterCon...
    method UpdateUserDetails (line 31) | internal void UpdateUserDetails(Steam.UserData userData, Steam.UserInf...
    method IsRedeemableApp (line 48) | internal bool IsRedeemableApp(FilterableApp app, HashSet<uint>? includ...
    method IsAppWantedByFilter (line 89) | internal bool IsAppWantedByFilter(FilterableApp app, FilterConfig filt...
    method IsAppIgnoredByFilter (line 139) | internal bool IsAppIgnoredByFilter(FilterableApp app, FilterConfig fil...
    method IsRedeemablePackage (line 189) | internal bool IsRedeemablePackage(FilterablePackage package, bool igno...
    method IsPackageWantedByFilter (line 246) | internal bool IsPackageWantedByFilter(FilterablePackage package, Filte...
    method IsPackageIgnoredByFilter (line 255) | internal bool IsPackageIgnoredByFilter(FilterablePackage package, Filt...
    method IsRedeemablePlaytest (line 272) | internal bool IsRedeemablePlaytest(FilterableApp app) {
    method IsPlaytestWantedByFilter (line 298) | internal bool IsPlaytestWantedByFilter(FilterableApp app, FilterConfig...
    method FilterOnlyAllowsPackages (line 329) | internal bool FilterOnlyAllowsPackages(FilterConfig filter) {
    method IsWantedApp (line 338) | internal bool IsWantedApp(FilterableApp app) {
    method IsWantedPackage (line 346) | internal bool IsWantedPackage(FilterablePackage package, bool ignoreAg...
    method IsWantedPlaytest (line 354) | internal bool IsWantedPlaytest(FilterableApp app) {
    method OwnsApp (line 362) | internal bool OwnsApp(uint appID) {
    method OwnsSub (line 370) | internal bool OwnsSub(uint subID) {

FILE: FreePackages/PackageQueue/ActivationQueue.cs
  class ActivationQueue (line 10) | internal sealed class ActivationQueue : PackageQueue {
    method ActivationQueue (line 19) | internal ActivationQueue(Bot bot, BotCache botCache, bool pauseWhilePl...
    method GetNextPackage (line 27) | protected override Package? GetNextPackage() => BotCache.GetNextPackag...
    method BeforeProcessing (line 29) | protected override async Task<DateTime?> BeforeProcessing(Package pack...
    method HandleResult (line 118) | protected override DateTime? HandleResult(Package package, EResult res...

FILE: FreePackages/PackageQueue/Package.cs
  class Package (line 6) | public sealed class Package {
    method ShouldSerializeStartTime (line 21) | public bool ShouldSerializeStartTime() => StartTime != null;
    method ShouldSerializeFilterHash (line 22) | public bool ShouldSerializeFilterHash() => FilterHash != null;
    method Package (line 24) | [JsonConstructor]
  type EPackageType (line 33) | public enum EPackageType {
  class PackageComparer (line 41) | public class PackageComparer : IEqualityComparer<Package> {
    method Equals (line 42) | public bool Equals(Package? x, Package? y) {
    method GetHashCode (line 46) | public int GetHashCode(Package obj) {

FILE: FreePackages/PackageQueue/PackageQueue.cs
  class PackageQueue (line 11) | internal abstract class PackageQueue : IDisposable {
    method PackageQueue (line 18) | internal PackageQueue(Bot bot, BotCache botCache, bool pauseWhilePlayi...
    method Dispose (line 25) | public void Dispose() {
    method Start (line 29) | internal void Start() {
    method ProcessQueue (line 33) | private async Task ProcessQueue() {
    method GetNextPackage (line 78) | protected abstract Package? GetNextPackage();
    method BeforeProcessing (line 80) | protected abstract Task<DateTime?> BeforeProcessing(Package package);
    method HandleResult (line 82) | protected abstract DateTime? HandleResult(Package package, EResult res...
    method ProcessPackage (line 84) | private async Task<EResult> ProcessPackage(Package package) {
    method ClaimFreeApp (line 108) | private async Task<EResult> ClaimFreeApp(uint appID) {
    method ClaimFreeSub (line 156) | private async Task<EResult> ClaimFreeSub(uint subID) {
    method ClaimPlaytest (line 197) | private async Task<EResult> ClaimPlaytest(uint appID) {
    method RemoveSub (line 230) | private async Task<EResult> RemoveSub(uint subID) {
    method RemoveApp (line 261) | private async Task<EResult> RemoveApp(uint appID) {
    method GetMillisecondsFromNow (line 292) | private static int GetMillisecondsFromNow(DateTime then) => Math.Max(0...
    method UpdateTimer (line 293) | private void UpdateTimer(DateTime then) => Timer?.Change(GetMillisecon...

FILE: FreePackages/PackageQueue/RemovalQueue.cs
  class RemovalQueue (line 10) | internal sealed class RemovalQueue(Bot bot, BotCache botCache, bool paus...
    method GetNextPackage (line 16) | protected override Package? GetNextPackage() => BotCache.GetNextPackag...
    method BeforeProcessing (line 18) | protected override Task<DateTime?> BeforeProcessing(Package package) =...
    method HandleResult (line 20) | protected override DateTime? HandleResult(Package package, EResult res...

FILE: FreePackages/WebRequest.cs
  class WebRequest (line 12) | internal static class WebRequest {
    method GetUserData (line 13) | internal static async Task<Steam.UserData?> GetUserData(Bot bot) {
    method RequestPlaytestAccess (line 20) | internal static async Task<Steam.PlaytestAccessResponse?> RequestPlayt...
    method GetAccountLicenses (line 29) | internal static async Task<IDocument?> GetAccountLicenses(Bot bot) {
    method GetUserInfo (line 36) | internal static async Task<Steam.UserInfo?> GetUserInfo(Bot bot) {

FILE: FreePackagesImporter/code.user.js
  function GetSetting (line 37) | function GetSetting(name) {
  function SetSetting (line 41) | function SetSetting(name, value) {
  function UpdatePackages (line 63) | function UpdatePackages() {
  function Finish (line 108) | function Finish() {
  function AddPackages (line 112) | async function AddPackages() {
  function SendASF (line 130) | async function SendASF(operation, path, http_method, target_bot, data = ...
  function BuildInterface (line 168) | function BuildInterface() {
  function ShowMessage (line 262) | function ShowMessage(message) {
  function UpdateInterface (line 269) | function UpdateInterface() {
Condensed preview — 113 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,057K chars).
[
  {
    "path": ".editorconfig",
    "chars": 3853,
    "preview": "root = true\n\n[*]\nindent_style = tab\ninsert_final_newline = true\ntrim_trailing_whitespace = true\n\n[*.cs]\ncsharp_prefer_br"
  },
  {
    "path": ".gitattributes",
    "chars": 129,
    "preview": "# Auto detect text files and perform LF normalization\n* text=auto\n\n*.sh text eol=lf\n\n# Custom for Visual Studio\n*.cs dif"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.md",
    "chars": 503,
    "preview": "---\nname: Bug report\nabout: Create a report to help us improve\ntitle: ''\nlabels: bug\nassignees: ''\n\n---\n\n**Describe the "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.md",
    "chars": 117,
    "preview": "---\nname: Feature request\nabout: Suggest an idea for this project\ntitle: ''\nlabels: enhancement\nassignees: ''\n\n---\n\n\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/question.md",
    "chars": 107,
    "preview": "---\nname: Question\nabout: Ask a question about the project\ntitle: ''\nlabels: question\nassignees: ''\n\n---\n\n\n"
  },
  {
    "path": ".github/RELEASE_TEMPLATE.md",
    "chars": 74,
    "preview": "This version requires ArchiSteamFarm VX.X.X.X or newer\n\n### Changelog\n\n- \n"
  },
  {
    "path": ".github/workflows/publish.yml",
    "chars": 2833,
    "preview": "name: publish\n\non: [push, pull_request]\n\nenv:\n  PLUGIN_NAME: \"FreePackages\"\n  DOTNET_SDK_VERSION: 10.0\n\njobs:\n  publish:"
  },
  {
    "path": ".gitignore",
    "chars": 9904,
    "preview": "#     _     ____   _____\n#    / \\   / ___| |  ___|\n#   / _ \\  \\___ \\ | |_\n#  / ___ \\  ___) ||  _|\n# /_/   \\_\\|____/ |_|\n"
  },
  {
    "path": ".gitmodules",
    "chars": 110,
    "preview": "[submodule \"ArchiSteamFarm\"]\n\tpath = ArchiSteamFarm\n\turl = https://github.com/JustArchiNET/ArchiSteamFarm.git\n"
  },
  {
    "path": "Directory.Build.props",
    "chars": 1799,
    "preview": "<Project>\n\t<Import Project=\"ArchiSteamFarm/Directory.Build.props\" />\n\n\t<PropertyGroup>\n\t\t<PluginName>FreePackages</Plugi"
  },
  {
    "path": "Directory.Packages.props",
    "chars": 83,
    "preview": "<Project>\n\t<Import Project=\"ArchiSteamFarm/Directory.Packages.props\" />\n</Project>\n"
  },
  {
    "path": "FreePackages/.gitignore",
    "chars": 5595,
    "preview": "## Ignore Visual Studio temporary files, build results, and\n## files generated by popular Visual Studio add-ons.\n##\n## G"
  },
  {
    "path": "FreePackages/Commands.cs",
    "chars": 18671,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.ComponentModel;\nusing System.Globalization;\nusing System.Li"
  },
  {
    "path": "FreePackages/Data/Cache/BotCache.cs",
    "chars": 8167,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Text.Json.Serialization"
  },
  {
    "path": "FreePackages/Data/Cache/GlobalCache.cs",
    "chars": 1910,
    "preview": "using System;\nusing System.IO;\nusing System.Text.Json.Serialization;\nusing System.Threading.Tasks;\nusing ArchiSteamFarm."
  },
  {
    "path": "FreePackages/Data/External/ASFInfo.cs",
    "chars": 3283,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Text.RegularExpressions"
  },
  {
    "path": "FreePackages/Data/External/CardApps.cs",
    "chars": 1657,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Text.Json;\nusing System.Text.Json.Serial"
  },
  {
    "path": "FreePackages/Data/PICS/ProductInfo.cs",
    "chars": 3359,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;"
  },
  {
    "path": "FreePackages/FreePackages.cs",
    "chars": 4958,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Composition;\nusing System.Threading.Tasks;\nusing ArchiStea"
  },
  {
    "path": "FreePackages/FreePackages.csproj",
    "chars": 788,
    "preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\n  <PropertyGroup>\n    <Authors>Citrinate</Authors>\n    <CoreCompileDependsOn>Prepare"
  },
  {
    "path": "FreePackages/Handlers/PICSHandler.cs",
    "chars": 5621,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;"
  },
  {
    "path": "FreePackages/Handlers/PackageHandler.cs",
    "chars": 23432,
    "preview": "using System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Thr"
  },
  {
    "path": "FreePackages/Handlers/SteamHandler.cs",
    "chars": 2037,
    "preview": "using System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Thr"
  },
  {
    "path": "FreePackages/Helpers/DeterministicHasher.cs",
    "chars": 2037,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\n\nnamespace FreePackages {\n\tinternal static class Dete"
  },
  {
    "path": "FreePackages/Helpers/StatusReporter.cs",
    "chars": 5491,
    "preview": "using System;\nusing System.Collections.Concurrent;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Tex"
  },
  {
    "path": "FreePackages/IPC/Api/FreePackagesController.cs",
    "chars": 12288,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.IO;\nusing System.Linq;\nusing System.Net;\nusing System.Threa"
  },
  {
    "path": "FreePackages/IPC/Requests/QueueLicensesRequest.cs",
    "chars": 441,
    "preview": "using System.Collections.Generic;\nusing System.Text.Json.Serialization;\n\nnamespace FreePackages.IPC {\n\tpublic sealed cla"
  },
  {
    "path": "FreePackages/IPC/Responses/FreeSubResponse.cs",
    "chars": 519,
    "preview": "using System.Text.Json.Serialization;\nusing SteamKit2;\n\nnamespace FreePackages.IPC {\n\tpublic sealed class FreeSubRespons"
  },
  {
    "path": "FreePackages/Json.cs",
    "chars": 4242,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Text.Json;\nusing System.Text.Json.Serialization;\n\nnamespace"
  },
  {
    "path": "FreePackages/Localization/README.md",
    "chars": 168,
    "preview": "If you'd like to help translate this plugin you can do so here: https://crowdin.com/project/freepackages\n\nContact me on "
  },
  {
    "path": "FreePackages/Localization/Strings.de-DE.resx",
    "chars": 11007,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<root>\n  <!--\n    Microsoft ResX Schema\n\n    Version 2.0\n\n    The primary goals o"
  },
  {
    "path": "FreePackages/Localization/Strings.resx",
    "chars": 13880,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<root>\n  <!--  \n    Microsoft ResX Schema \n    \n    Version 2.0\n    \n    The prim"
  },
  {
    "path": "FreePackages/Localization/Strings.ru-RU.resx",
    "chars": 14303,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<root>\n  <!--  \n    Microsoft ResX Schema \n    \n    Version 2.0\n    \n    The prim"
  },
  {
    "path": "FreePackages/Localization/Strings.tr-TR.resx",
    "chars": 13991,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<root>\n  <!--  \n    Microsoft ResX Schema \n    \n    Version 2.0\n    \n    The prim"
  },
  {
    "path": "FreePackages/Localization/Strings.uk-UA.resx",
    "chars": 10883,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<root>\n  <!--\n    Microsoft ResX Schema\n\n    Version 2.0\n\n    The primary goals o"
  },
  {
    "path": "FreePackages/Localization/Strings.zh-Hans.resx",
    "chars": 10022,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<root>\n  <!--\n    Microsoft ResX Schema\n\n    Version 2.0\n\n    The primary goals o"
  },
  {
    "path": "FreePackages/Localization/Strings.zh-Hant.resx",
    "chars": 10023,
    "preview": "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n<root>\n  <!--\n    Microsoft ResX Schema\n\n    Version 2.0\n\n    The primary goals o"
  },
  {
    "path": "FreePackages/PackageFilter/FilterConfig.cs",
    "chars": 3420,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Text.Json.Serialization;\n\nnamespace FreePackages {\n\tinterna"
  },
  {
    "path": "FreePackages/PackageFilter/Filterables/FilterableApp.cs",
    "chars": 11514,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing ArchiSteamFarm.Co"
  },
  {
    "path": "FreePackages/PackageFilter/Filterables/FilterablePackage.cs",
    "chars": 8145,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;"
  },
  {
    "path": "FreePackages/PackageFilter/PackageFilter.cs",
    "chars": 12540,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing AngleSharp.Dom;\nusing ArchiSteamFarm.Core;\nusin"
  },
  {
    "path": "FreePackages/PackageQueue/ActivationQueue.cs",
    "chars": 6088,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing ArchiSteamFarm.St"
  },
  {
    "path": "FreePackages/PackageQueue/Package.cs",
    "chars": 1159,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Text.Json.Serialization;\n\nnamespace FreePackages {\n\tpublic "
  },
  {
    "path": "FreePackages/PackageQueue/PackageQueue.cs",
    "chars": 9888,
    "preview": "using System;\nusing System.Linq;\nusing System.Threading;\nusing System.Threading.Tasks;\nusing ArchiSteamFarm.Core;\nusing "
  },
  {
    "path": "FreePackages/PackageQueue/RemovalQueue.cs",
    "chars": 1562,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Linq;\nusing System.Threading.Tasks;\nusing ArchiSteamFarm.St"
  },
  {
    "path": "FreePackages/WebRequest.cs",
    "chars": 2530,
    "preview": "using System;\nusing System.Collections.Generic;\nusing System.Text.RegularExpressions;\nusing System.Threading.Tasks;\nusin"
  },
  {
    "path": "FreePackages.Tests/Apps.cs",
    "chars": 3514,
    "preview": "using System.Collections.Generic;\nusing System.IO;\nusing ArchiSteamFarm.Helpers.Json;\nusing Microsoft.IdentityModel.Toke"
  },
  {
    "path": "FreePackages.Tests/Filters.cs",
    "chars": 15145,
    "preview": "using System.Collections.Generic;\nusing System.IO;\nusing ArchiSteamFarm.Helpers.Json;\nusing Microsoft.VisualStudio.TestT"
  },
  {
    "path": "FreePackages.Tests/FreePackages.Tests.csproj",
    "chars": 424,
    "preview": "<Project Sdk=\"Microsoft.NET.Sdk\">\n\t  <PropertyGroup>\n        <IsTestProject>true</IsTestProject>\n    </PropertyGroup>\n\n\t"
  },
  {
    "path": "FreePackages.Tests/Packages.cs",
    "chars": 4107,
    "preview": "using System;\nusing System.IO;\nusing System.Threading.Tasks;\nusing ArchiSteamFarm.Helpers.Json;\nusing Microsoft.VisualSt"
  },
  {
    "path": "FreePackages.Tests/TestData/app_which_is_free.txt",
    "chars": 48636,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"440\"\n\t\"common\"\n\t{\n\t\t\"icon\"\t\t\"e3f595a92552da3d664ad00277fad2107345f743\"\n\t\t\"logo\"\t\t\"07385eb55b5ba97"
  },
  {
    "path": "FreePackages.Tests/TestData/app_with_categories.txt",
    "chars": 48636,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"440\"\n\t\"common\"\n\t{\n\t\t\"icon\"\t\t\"e3f595a92552da3d664ad00277fad2107345f743\"\n\t\t\"logo\"\t\t\"07385eb55b5ba97"
  },
  {
    "path": "FreePackages.Tests/TestData/app_with_content_descriptors.txt",
    "chars": 48636,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"440\"\n\t\"common\"\n\t{\n\t\t\"icon\"\t\t\"e3f595a92552da3d664ad00277fad2107345f743\"\n\t\t\"logo\"\t\t\"07385eb55b5ba97"
  },
  {
    "path": "FreePackages.Tests/TestData/app_with_deck_playable.txt",
    "chars": 7822,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"30\"\n\t\"common\"\n\t{\n\t\t\"clienticon\"\t\t\"d2b202e0eeb70d84fdb80f6cf886c03064011f49\"\n\t\t\"clienttga\"\t\t\"f89e2"
  },
  {
    "path": "FreePackages.Tests/TestData/app_with_deck_unknown.txt",
    "chars": 3179,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"1449570\"\n\t\"common\"\n\t{\n\t\t\"name\"\t\t\"The Godkiller - Chapter 1\"\n\t\t\"type\"\t\t\"Game\"\n\t\t\"oslist\"\t\t\"windows"
  },
  {
    "path": "FreePackages.Tests/TestData/app_with_deck_unsupported.txt",
    "chars": 17746,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"43160\"\n\t\"common\"\n\t{\n\t\t\"name\"\t\t\"Metro: Last Light Complete Edition\"\n\t\t\"logo\"\t\t\"b110ee3812ac0cad685"
  },
  {
    "path": "FreePackages.Tests/TestData/app_with_deck_verified.txt",
    "chars": 207552,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"1086940\"\n\t\"common\"\n\t{\n\t\t\"name\"\t\t\"Baldur's Gate 3\"\n\t\t\"type\"\t\t\"Game\"\n\t\t\"releasestate\"\t\t\"released\"\n\t"
  },
  {
    "path": "FreePackages.Tests/TestData/app_with_dlc.txt",
    "chars": 153873,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"34330\"\n\t\"common\"\n\t{\n\t\t\"name\"\t\t\"Total War: SHOGUN 2\"\n\t\t\"clienticon\"\t\t\"71a76cd2fbcd1457887fe57727aa"
  },
  {
    "path": "FreePackages.Tests/TestData/app_with_language_support.txt",
    "chars": 48636,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"440\"\n\t\"common\"\n\t{\n\t\t\"icon\"\t\t\"e3f595a92552da3d664ad00277fad2107345f743\"\n\t\t\"logo\"\t\t\"07385eb55b5ba97"
  },
  {
    "path": "FreePackages.Tests/TestData/app_with_purchase_restricted_countries.txt",
    "chars": 6454,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"212200\"\n\t\"common\"\n\t{\n\t\t\"name\"\t\t\"Mabinogi\"\n\t\t\"type\"\t\t\"Game\"\n\t\t\"linuxclienticon\"\t\t\"9a431be93206e256"
  },
  {
    "path": "FreePackages.Tests/TestData/app_with_release_state.txt",
    "chars": 170085,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"1086940\"\n\t\"common\"\n\t{\n\t\t\"name\"\t\t\"Baldur's Gate 3\"\n\t\t\"type\"\t\t\"Game\"\n\t\t\"releasestate\"\t\t\"released\"\n\t"
  },
  {
    "path": "FreePackages.Tests/TestData/app_with_required_app.txt",
    "chars": 2291,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"2378500\"\n\t\"common\"\n\t{\n\t\t\"name\"\t\t\"Digital Deluxe Edition DLC\"\n\t\t\"type\"\t\t\"DLC\"\n\t\t\"parent\"\t\t\"1086940"
  },
  {
    "path": "FreePackages.Tests/TestData/app_with_restricted_countries.txt",
    "chars": 3295,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"1245610\"\n\t\"common\"\n\t{\n\t\t\"name\"\t\t\"AChat\"\n\t\t\"type\"\t\t\"Game\"\n\t\t\"releasestate\"\t\t\"released\"\n\t\t\"logo\"\t\t\""
  },
  {
    "path": "FreePackages.Tests/TestData/app_with_review_score.txt",
    "chars": 48636,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"440\"\n\t\"common\"\n\t{\n\t\t\"icon\"\t\t\"e3f595a92552da3d664ad00277fad2107345f743\"\n\t\t\"logo\"\t\t\"07385eb55b5ba97"
  },
  {
    "path": "FreePackages.Tests/TestData/app_with_state.txt",
    "chars": 48636,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"440\"\n\t\"common\"\n\t{\n\t\t\"icon\"\t\t\"e3f595a92552da3d664ad00277fad2107345f743\"\n\t\t\"logo\"\t\t\"07385eb55b5ba97"
  },
  {
    "path": "FreePackages.Tests/TestData/app_with_tags.txt",
    "chars": 48636,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"440\"\n\t\"common\"\n\t{\n\t\t\"icon\"\t\t\"e3f595a92552da3d664ad00277fad2107345f743\"\n\t\t\"logo\"\t\t\"07385eb55b5ba97"
  },
  {
    "path": "FreePackages.Tests/TestData/app_with_type.txt",
    "chars": 48636,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"440\"\n\t\"common\"\n\t{\n\t\t\"icon\"\t\t\"e3f595a92552da3d664ad00277fad2107345f743\"\n\t\t\"logo\"\t\t\"07385eb55b5ba97"
  },
  {
    "path": "FreePackages.Tests/TestData/demo_which_will_be_removed.txt",
    "chars": 1912,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"2390760\"\n\t\"common\"\n\t{\n\t\t\"name\"\t\t\"Headlong Hunt Demo\"\n\t\t\"type\"\t\t\"Demo\"\n\t\t\"parent\"\t\t\"2323660\"\n\t\t\"ic"
  },
  {
    "path": "FreePackages.Tests/TestData/demo_with_fewer_categories_than_parent.txt",
    "chars": 4889,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"410\"\n\t\"common\"\n\t{\n\t\t\"clienticon\"\t\t\"c7cb09b9f0fbb9589b4bd5a8217c8333c4d8204e\"\n\t\t\"clienttga\"\t\t\"0da3"
  },
  {
    "path": "FreePackages.Tests/TestData/demo_with_fewer_categories_than_parent_parent.txt",
    "chars": 13667,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"400\"\n\t\"common\"\n\t{\n\t\t\"clienticon\"\t\t\"c7cb09b9f0fbb9589b4bd5a8217c8333c4d8204e\"\n\t\t\"clienttga\"\t\t\"0da3"
  },
  {
    "path": "FreePackages.Tests/TestData/demo_with_fewer_content_descriptors_than_parent.txt",
    "chars": 4517,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"547490\"\n\t\"common\"\n\t{\n\t\t\"name\"\t\t\"Shadow Tactics: Blades of the Shogun Demo\"\n\t\t\"type\"\t\t\"Demo\"\n\t\t\"pa"
  },
  {
    "path": "FreePackages.Tests/TestData/demo_with_fewer_content_descriptors_than_parent_parent.txt",
    "chars": 19417,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"418240\"\n\t\"common\"\n\t{\n\t\t\"name\"\t\t\"Shadow Tactics: Blades of the Shogun\"\n\t\t\"type\"\t\t\"Game\"\n\t\t\"oslist\""
  },
  {
    "path": "FreePackages.Tests/TestData/demo_with_fewer_languages_than_parent.txt",
    "chars": 6296,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"1316010\"\n\t\"common\"\n\t{\n\t\t\"name\"\t\t\"Grounded Demo\"\n\t\t\"type\"\t\t\"Demo\"\n\t\t\"parent\"\t\t\"962130\"\n\t\t\"releases"
  },
  {
    "path": "FreePackages.Tests/TestData/demo_with_fewer_languages_than_parent_parent.txt",
    "chars": 21099,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"962130\"\n\t\"common\"\n\t{\n\t\t\"name\"\t\t\"Grounded\"\n\t\t\"type\"\t\t\"Game\"\n\t\t\"oslist\"\t\t\"windows\"\n\t\t\"osarch\"\t\t\"64\""
  },
  {
    "path": "FreePackages.Tests/TestData/demo_with_fewer_tags_than_parent.txt",
    "chars": 4889,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"410\"\n\t\"common\"\n\t{\n\t\t\"clienticon\"\t\t\"c7cb09b9f0fbb9589b4bd5a8217c8333c4d8204e\"\n\t\t\"clienttga\"\t\t\"0da3"
  },
  {
    "path": "FreePackages.Tests/TestData/demo_with_fewer_tags_than_parent_parent.txt",
    "chars": 13667,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"400\"\n\t\"common\"\n\t{\n\t\t\"clienticon\"\t\t\"c7cb09b9f0fbb9589b4bd5a8217c8333c4d8204e\"\n\t\t\"clienttga\"\t\t\"0da3"
  },
  {
    "path": "FreePackages.Tests/TestData/package_which_is_free.txt",
    "chars": 243,
    "preview": "\"953346\"\n{\n\t\"packageid\"\t\t\"953346\"\n\t\"billingtype\"\t\t\"12\"\n\t\"licensetype\"\t\t\"1\"\n\t\"status\"\t\t\"0\"\n\t\"extended\"\n\t{\n\t\t\"allowcrossre"
  },
  {
    "path": "FreePackages.Tests/TestData/package_which_is_no_cost.txt",
    "chars": 323,
    "preview": "\"44911\"\n{\n\t\"packageid\"\t\t\"44911\"\n\t\"billingtype\"\t\t\"0\"\n\t\"licensetype\"\t\t\"1\"\n\t\"status\"\t\t\"2\"\n\t\"extended\"\n\t{\n\t\t\"allowpurchasefr"
  },
  {
    "path": "FreePackages.Tests/TestData/package_with_deactivated_demo.txt",
    "chars": 327,
    "preview": "\"20737\"\n{\n\t\"packageid\"\t\t\"20737\"\n\t\"billingtype\"\t\t\"12\"\n\t\"licensetype\"\t\t\"1\"\n\t\"status\"\t\t\"0\"\n\t\"extended\"\n\t{\n\t\t\"deactivated_de"
  },
  {
    "path": "FreePackages.Tests/TestData/package_with_demo_which_will_be_removed.txt",
    "chars": 196,
    "preview": "\"860092\"\n{\n\t\"packageid\"\t\t\"860092\"\n\t\"billingtype\"\t\t\"12\"\n\t\"licensetype\"\t\t\"1\"\n\t\"status\"\t\t\"0\"\n\t\"extended\"\n\t{\n\t}\n\t\"appids\"\n\t{"
  },
  {
    "path": "FreePackages.Tests/TestData/package_with_disallowed_app.txt",
    "chars": 337,
    "preview": "\"657460\"\n{\n\t\"packageid\"\t\t\"657460\"\n\t\"billingtype\"\t\t\"12\"\n\t\"licensetype\"\t\t\"1\"\n\t\"status\"\t\t\"0\"\n\t\"extended\"\n\t{\n\t\t\"deactivated_"
  },
  {
    "path": "FreePackages.Tests/TestData/package_with_free_weekend.txt",
    "chars": 434,
    "preview": "\"81948\"\n{\n\t\"packageid\"\t\t\"81948\"\n\t\"billingtype\"\t\t\"12\"\n\t\"licensetype\"\t\t\"1\"\n\t\"status\"\t\t\"0\"\n\t\"extended\"\n\t{\n\t\t\"dontgrantifapp"
  },
  {
    "path": "FreePackages.Tests/TestData/package_with_purchase_restricted_countries.txt",
    "chars": 385,
    "preview": "\"1890\"\n{\n\t\"packageid\"\t\t\"1890\"\n\t\"billingtype\"\t\t\"1\"\n\t\"licensetype\"\t\t\"1\"\n\t\"status\"\t\t\"0\"\n\t\"extended\"\n\t{\n\t\t\"allowpurchasefrom"
  },
  {
    "path": "FreePackages.Tests/TestData/package_with_restricted_countries.txt",
    "chars": 391,
    "preview": "\"178\"\n{\n\t\"packageid\"\t\t\"178\"\n\t\"billingtype\"\t\t\"1\"\n\t\"licensetype\"\t\t\"1\"\n\t\"status\"\t\t\"0\"\n\t\"extended\"\n\t{\n\t\t\"allowpurchasefromre"
  },
  {
    "path": "FreePackages.Tests/TestData/package_with_single_app.txt",
    "chars": 774,
    "preview": "\"907539\"\n{\n\t\"packageid\"\t\t\"907539\"\n\t\"billingtype\"\t\t\"10\"\n\t\"licensetype\"\t\t\"1\"\n\t\"status\"\t\t\"0\"\n\t\"extended\"\n\t{\n\t\t\"allowcrossre"
  },
  {
    "path": "FreePackages.Tests/TestData/package_with_single_app_app_1.txt",
    "chars": 170085,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"1086940\"\n\t\"common\"\n\t{\n\t\t\"name\"\t\t\"Baldur's Gate 3\"\n\t\t\"type\"\t\t\"Game\"\n\t\t\"releasestate\"\t\t\"released\"\n\t"
  },
  {
    "path": "FreePackages.Tests/TestData/package_with_timed_activation.txt",
    "chars": 327,
    "preview": "\"20737\"\n{\n\t\"packageid\"\t\t\"20737\"\n\t\"billingtype\"\t\t\"12\"\n\t\"licensetype\"\t\t\"1\"\n\t\"status\"\t\t\"0\"\n\t\"extended\"\n\t{\n\t\t\"deactivated_de"
  },
  {
    "path": "FreePackages.Tests/TestData/playtest_with_hidden_parent.txt",
    "chars": 1778,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"2423370\"\n\t\"common\"\n\t{\n\t\t\"name\"\t\t\"Inu (Prototype) Playtest\"\n\t\t\"type\"\t\t\"Beta\"\n\t\t\"parent\"\t\t\"2423350\""
  },
  {
    "path": "FreePackages.Tests/TestData/playtest_with_hidden_parent_parent.txt",
    "chars": 54,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"2423350\"\n\t\"public_only\"\t\t\"1\"\n}\n"
  },
  {
    "path": "FreePackages.Tests/TestData/playtest_with_no_categories.txt",
    "chars": 7090,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"2385860\"\n\t\"common\"\n\t{\n\t\t\"name\"\t\t\"TEKKEN 8 Playtest\"\n\t\t\"type\"\t\t\"Beta\"\n\t\t\"parent\"\t\t\"1778820\"\n\t\t\"osl"
  },
  {
    "path": "FreePackages.Tests/TestData/playtest_with_no_categories_parent.txt",
    "chars": 3270,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"1778820\"\n\t\"public_only\"\t\t\"1\"\n\t\"common\"\n\t{\n\t\t\"name\"\t\t\"TEKKEN 8\"\n\t\t\"type\"\t\t\"Game\"\n\t\t\"icon\"\t\t\"379f0b"
  },
  {
    "path": "FreePackages.Tests/TestData/playtest_with_no_languages.txt",
    "chars": 7090,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"2385860\"\n\t\"common\"\n\t{\n\t\t\"name\"\t\t\"TEKKEN 8 Playtest\"\n\t\t\"type\"\t\t\"Beta\"\n\t\t\"parent\"\t\t\"1778820\"\n\t\t\"osl"
  },
  {
    "path": "FreePackages.Tests/TestData/playtest_with_no_languages_parent.txt",
    "chars": 3270,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"1778820\"\n\t\"public_only\"\t\t\"1\"\n\t\"common\"\n\t{\n\t\t\"name\"\t\t\"TEKKEN 8\"\n\t\t\"type\"\t\t\"Game\"\n\t\t\"icon\"\t\t\"379f0b"
  },
  {
    "path": "FreePackages.Tests/TestData/playtest_with_no_waitlist.txt",
    "chars": 2352,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"2437370\"\n\t\"common\"\n\t{\n\t\t\"name\"\t\t\"No Love Lost Playtest\"\n\t\t\"type\"\t\t\"Beta\"\n\t\t\"parent\"\t\t\"1873120\"\n\t\t"
  },
  {
    "path": "FreePackages.Tests/TestData/playtest_with_no_waitlist_parent.txt",
    "chars": 1778,
    "preview": "\"appinfo\"\n{\n\t\"appid\"\t\t\"1873120\"\n\t\"public_only\"\t\t\"1\"\n\t\"common\"\n\t{\n\t\t\"name\"\t\t\"No Love Lost\"\n\t\t\"type\"\t\t\"Game\"\n\t\t\"oslist\"\t\t\""
  },
  {
    "path": "FreePackages.Tests/TestData/userdata_empty.json",
    "chars": 746,
    "preview": "{\n    \"rgWishlist\":[],\n    \"rgOwnedPackages\":[],\n    \"rgOwnedApps\":[],\n    \"rgFollowedApps\":[],\n    \"rgMasterSubApps\":[]"
  },
  {
    "path": "FreePackages.Tests/TestData/userdata_with_excluded_content_descriptors.json",
    "chars": 747,
    "preview": "{\n    \"rgWishlist\":[],\n    \"rgOwnedPackages\":[],\n    \"rgOwnedApps\":[],\n    \"rgFollowedApps\":[],\n    \"rgMasterSubApps\":[]"
  },
  {
    "path": "FreePackages.Tests/TestData/userdata_with_excluded_tags.json",
    "chars": 810,
    "preview": "{\n    \"rgWishlist\":[],\n    \"rgOwnedPackages\":[],\n    \"rgOwnedApps\":[],\n    \"rgFollowedApps\":[],\n    \"rgMasterSubApps\":[]"
  },
  {
    "path": "FreePackages.Tests/TestData/userdata_with_followed_apps.json",
    "chars": 749,
    "preview": "{\n    \"rgWishlist\":[],\n    \"rgOwnedPackages\":[],\n    \"rgOwnedApps\":[],\n    \"rgFollowedApps\":[440],\n    \"rgMasterSubApps\""
  },
  {
    "path": "FreePackages.Tests/TestData/userdata_with_ignored_apps.json",
    "chars": 762,
    "preview": "{\n    \"rgWishlist\":[],\n    \"rgOwnedPackages\":[],\n    \"rgOwnedApps\":[],\n    \"rgFollowedApps\":[],\n    \"rgMasterSubApps\":[]"
  },
  {
    "path": "FreePackages.Tests/TestData/userdata_with_wishlist_apps.json",
    "chars": 749,
    "preview": "{\n    \"rgWishlist\":[440],\n    \"rgOwnedPackages\":[],\n    \"rgOwnedApps\":[],\n    \"rgFollowedApps\":[],\n    \"rgMasterSubApps\""
  },
  {
    "path": "FreePackages.Tests/TestData/userinfo_empty.json",
    "chars": 224,
    "preview": "{\n\t\"logged_in\":false,\n\t\"steamid\":\"\",\n\t\"accountid\":0,\n\t\"account_name\":\"\",\n\t\"is_support\":false,\n\t\"is_limited\":false,\n\t\"is_"
  },
  {
    "path": "FreePackages.Tests/generate_test_data.sh",
    "chars": 2909,
    "preview": "#!/bin/bash\n\nASF_SERVER=${1:-\"http://localhost\"}\nASF_PORT=${2:-\"1242\"}\nASF_PASSWORD=${3:-\"\"}\nURL=\"$ASF_SERVER:$ASF_PORT/"
  },
  {
    "path": "FreePackages.sln",
    "chars": 2118,
    "preview": "\nMicrosoft Visual Studio Solution File, Format Version 12.00\n# Visual Studio 15\nVisualStudioVersion = 15.0.28307.136\nMi"
  },
  {
    "path": "FreePackagesImporter/README.md",
    "chars": 1632,
    "preview": "# Free Packages Importer\n\nThis userscript lets you transfer packages from SteamDB's [free packages tool](https://steamdb"
  },
  {
    "path": "FreePackagesImporter/code.user.js",
    "chars": 12851,
    "preview": "// ==UserScript==\n// @name        Free Packages Importer\n// @namespace   https://github.com/Citrinate\n// @author      Ci"
  },
  {
    "path": "LICENSE",
    "chars": 11357,
    "preview": "\n                                 Apache License\n                           Version 2.0, January 2004\n                  "
  },
  {
    "path": "README.md",
    "chars": 21599,
    "preview": "# Free Packages Plugin for ArchiSteamFarm\n\n[![Check out my other ArchiSteamFarm plugins](https://img.shields.io/badge/Ch"
  },
  {
    "path": "SECURITY.md",
    "chars": 199,
    "preview": "# Security Policy\n\n## Supported Versions\n\nOnly the latest version is supported.\n\n## Reporting a Vulnerability\n\nPlease re"
  },
  {
    "path": "build.bat",
    "chars": 1405,
    "preview": "@echo off\nrem getting current dir name by Tamara Wijsman, https://superuser.com/questions/160702\nfor %%I in (.) do set C"
  },
  {
    "path": "build.sh",
    "chars": 1929,
    "preview": "#!/bin/bash\n\n## https://github.com/Ryzhehvost/asf_plugin_creator\n\n######################################################"
  },
  {
    "path": "crowdin.yml",
    "chars": 125,
    "preview": "files:\n  - source: /FreePackages/Localization/Strings.resx\n    translation: /FreePackages/Localization/Strings.%locale%."
  },
  {
    "path": "github-pandoc.css",
    "chars": 15991,
    "preview": "/*! normalize.css v2.1.3 | MIT License | git.io/normalize */\n\n/* ======================================================="
  }
]

About this extraction

This page contains the full source code of the Citrinate/FreePackages GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 113 files (1.5 MB), approximately 702.9k tokens, and a symbol index with 300 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!